// The libMesh Finite Element Library.
// Copyright (C) 2002-2021 Benjamin S. Kirk, John W. Peterson, Roy H. Stogner

// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.

// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA


#include "libmesh/exodusII_io_helper.h"


#ifdef LIBMESH_HAVE_EXODUS_API

// libMesh includes
#include "libmesh/boundary_info.h"
#include "libmesh/enum_elem_type.h"
#include "libmesh/elem.h"
#include "libmesh/system.h"
#include "libmesh/numeric_vector.h"
#include "libmesh/enum_to_string.h"
#include "libmesh/enum_elem_type.h"
#include "libmesh/int_range.h"
#include "libmesh/utility.h"

#ifdef DEBUG
#include "libmesh/mesh_tools.h"  // for elem_types warning
#endif

#include <libmesh/ignore_warnings.h>
namespace exII {
extern "C" {
#include "vtk_exodusII.h" // defines MAX_LINE_LENGTH, MAX_STR_LENGTH used later
}
}
#include <libmesh/restore_warnings.h>

// C++ includes
#include <algorithm>
#include <sstream>
#include <cstdlib> // std::strtol
#include <unordered_map>

// Anonymous namespace for file local data
namespace
{

// File scope constant node/edge/face mapping arrays.
// 2D inverse face map definitions.
// These take a libMesh ID and turn it into an Exodus ID
const std::vector<int> trishell3_inverse_edge_map = {3, 4, 5};
const std::vector<int> quadshell4_inverse_edge_map = {3, 4, 5, 6};

// 3D node map definitions
// The hex27 appears to be the only element without an identity mapping between its
// node numbering and libmesh's.
const std::vector<int> hex27_node_map = {
  // Vertex and mid-edge nodes
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
  // Mid-face nodes and center node
  21, 25, 24, 26, 23, 22, 20};
//20  21  22  23  24  25  26 // LibMesh indices

const std::vector<int> hex27_inverse_node_map = {
  // Vertex and mid-edge nodes
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
  // Mid-face nodes and center node
  26, 20, 25, 24, 22, 21, 23};
//20  21  22  23  24  25  26

// 3D face map definitions
const std::vector<int> tet_face_map = {1, 2, 3, 0};
const std::vector<int> hex_face_map = {1, 2, 3, 4, 0, 5};
const std::vector<int> prism_face_map = {1, 2, 3, 0, 4};

// These take a libMesh ID and turn it into an Exodus ID
const std::vector<int> tet_inverse_face_map = {4, 1, 2, 3};
const std::vector<int> hex_inverse_face_map = {5, 1, 2, 3, 4, 6};
const std::vector<int> prism_inverse_face_map = {4, 1, 2, 3, 5};

// 3D element edge maps. Map 0-based Exodus id -> libMesh id.
const std::vector<int> hex_edge_map =
  {0,1,2,3,8,9,10,11,4,5,7,6};

// 3D inverse element edge maps. Map libmesh edge ids to 1-based Exodus edge ids.
const std::vector<int> hex_inverse_edge_map =
  {1,2,3,4,9,10,12,11,5,6,7,8};

  /**
   * \returns The value obtained from a generic exII::ex_inquire() call.
   */
  int inquire(libMesh::ExodusII_IO_Helper & e2h, exII::ex_inquiry req_info_in, std::string error_msg="")
  {
    int ret_int = 0;
    char ret_char = 0;
    float ret_float = 0.;

    e2h.ex_error = exII::ex_inquire(e2h.ex_id,
                                  req_info_in,
                                  &ret_int,
                                  &ret_float,
                                  &ret_char);

    EX_CHECK_ERR(e2h.ex_error, error_msg);

    return ret_int;
  }

} // end anonymous namespace



namespace libMesh
{

// ExodusII_IO_Helper::Conversion static data
const int ExodusII_IO_Helper::Conversion::invalid_id = std::numeric_limits<int>::max();

ExodusII_IO_Helper::ExodusII_IO_Helper(const ParallelObject & parent,
                                       bool v,
                                       bool run_only_on_proc0,
                                       bool single_precision) :
  ParallelObject(parent),
  ex_id(0),
  ex_error(0),
  header_info(), // zero-initialize
  title(header_info.title),
  num_dim(header_info.num_dim),
  num_nodes(header_info.num_nodes),
  num_elem(header_info.num_elem),
  num_elem_blk(header_info.num_elem_blk),
  num_edge(header_info.num_edge),
  num_edge_blk(header_info.num_edge_blk),
  num_node_sets(header_info.num_node_sets),
  num_side_sets(header_info.num_side_sets),
  num_global_vars(0),
  num_sideset_vars(0),
  num_elem_this_blk(0),
  num_nodes_per_elem(0),
  num_attr(0),
  num_elem_all_sidesets(0),
  num_time_steps(0),
  num_nodal_vars(0),
  num_elem_vars(0),
  verbose(v),
  opened_for_writing(false),
  opened_for_reading(false),
  _run_only_on_proc0(run_only_on_proc0),
  _elem_vars_initialized(false),
  _global_vars_initialized(false),
  _nodal_vars_initialized(false),
  _use_mesh_dimension_instead_of_spatial_dimension(false),
  _write_as_dimension(0),
  _single_precision(single_precision)
{
  title.resize(MAX_LINE_LENGTH+1);
  elem_type.resize(MAX_STR_LENGTH);
  init_element_equivalence_map();
  init_conversion_map();
}



ExodusII_IO_Helper::~ExodusII_IO_Helper() = default;



// Initialization function for conversion_map object
void ExodusII_IO_Helper::init_conversion_map()
{
  {
    auto & conv = conversion_map[NODEELEM];
    conv.libmesh_type = NODEELEM;
    conv.exodus_type = "SPHERE";
  }
  {
    auto & conv = conversion_map[EDGE2];
    conv.libmesh_type = EDGE2;
    conv.exodus_type = "EDGE2";
  }
  {
    auto & conv = conversion_map[EDGE3];
    conv.libmesh_type = EDGE3;
    conv.exodus_type = "EDGE3";
  }
  {
    auto & conv = conversion_map[QUAD4];
    conv.libmesh_type = QUAD4;
    conv.exodus_type = "QUAD4";
  }
  {
    auto & conv = conversion_map[QUADSHELL4];
    conv.inverse_side_map = &quadshell4_inverse_edge_map;
    conv.shellface_index_offset = 2;
    conv.libmesh_type = QUADSHELL4;
    conv.exodus_type = "SHELL4";
  }
  {
    auto & conv = conversion_map[QUAD8];
    conv.libmesh_type = QUAD8;
    conv.exodus_type = "QUAD8";
  }
  {
    auto & conv = conversion_map[QUADSHELL8];
    conv.inverse_side_map = &quadshell4_inverse_edge_map;
    conv.shellface_index_offset = 2;
    conv.libmesh_type = QUADSHELL8;
    conv.exodus_type = "SHELL8";
  }
  {
    auto & conv = conversion_map[QUAD9];
    conv.libmesh_type = QUAD9;
    conv.exodus_type = "QUAD9";
  }
  {
    auto & conv = conversion_map[TRI3];
    conv.libmesh_type = TRI3;
    conv.exodus_type = "TRI3";
  }
  {
    auto & conv = conversion_map[TRISHELL3];
    conv.inverse_side_map = &trishell3_inverse_edge_map;
    conv.shellface_index_offset = 2;
    conv.libmesh_type = TRISHELL3;
    conv.exodus_type = "TRISHELL3";
  }
  {
    auto & conv = conversion_map[TRI3SUBDIVISION];
    conv.libmesh_type = TRI3SUBDIVISION;
    conv.exodus_type = "TRI3";
  }
  {
    auto & conv = conversion_map[TRI6];
    conv.libmesh_type = TRI6;
    conv.exodus_type = "TRI6";
  }
  {
    auto & conv = conversion_map[HEX8];
    conv.side_map = &hex_face_map;
    conv.inverse_side_map = &hex_inverse_face_map;
    conv.libmesh_type = HEX8;
    conv.exodus_type = "HEX8";
  }
  {
    auto & conv = conversion_map[HEX20];
    conv.side_map = &hex_face_map;
    conv.inverse_side_map = &hex_inverse_face_map;
    conv.libmesh_type = HEX20;
    conv.exodus_type = "HEX20";
  }
  {
    auto & conv = conversion_map[HEX27];
    conv.node_map = &hex27_node_map;
    conv.inverse_node_map = &hex27_inverse_node_map;
    conv.side_map = &hex_face_map;
    conv.inverse_side_map = &hex_inverse_face_map;
    conv.libmesh_type = HEX27;
    conv.exodus_type = "HEX27";
  }
  {
    auto & conv = conversion_map[TET4];
    conv.side_map = &tet_face_map;
    conv.inverse_side_map = &tet_inverse_face_map;
    conv.libmesh_type = TET4;
    conv.exodus_type = "TETRA4";
  }
  {
    auto & conv = conversion_map[TET10];
    conv.side_map = &tet_face_map;
    conv.inverse_side_map = &tet_inverse_face_map;
    conv.libmesh_type = TET10;
    conv.exodus_type = "TETRA10";
  }
  {
    auto & conv = conversion_map[PRISM6];
    conv.side_map = &prism_face_map;
    conv.inverse_side_map = &prism_inverse_face_map;
    conv.libmesh_type = PRISM6;
    conv.exodus_type = "WEDGE";
  }
  {
    auto & conv = conversion_map[PRISM15];
    conv.side_map = &prism_face_map;
    conv.inverse_side_map = &prism_inverse_face_map;
    conv.libmesh_type = PRISM15;
    conv.exodus_type = "WEDGE15";
  }
  {
    auto & conv = conversion_map[PRISM18];
    conv.side_map = &prism_face_map;
    conv.inverse_side_map = &prism_inverse_face_map;
    conv.libmesh_type = PRISM18;
    conv.exodus_type = "WEDGE18";
  }
  {
    auto & conv = conversion_map[PYRAMID5];
    conv.libmesh_type = PYRAMID5;
    conv.exodus_type = "PYRAMID5";
  }
  {
    auto & conv = conversion_map[PYRAMID13];
    conv.libmesh_type = PYRAMID13;
    conv.exodus_type = "PYRAMID13";
  }
  {
    auto & conv = conversion_map[PYRAMID14];
    conv.libmesh_type = PYRAMID14;
    conv.exodus_type = "PYRAMID14";
  }
}



// This function initializes the element_equivalence_map the first time it
// is called, and returns early all other times.
void ExodusII_IO_Helper::init_element_equivalence_map()
{
  // We use an ExodusII SPHERE element to represent a NodeElem
  element_equivalence_map["SPHERE"] = NODEELEM;

  // EDGE2 equivalences
  element_equivalence_map["EDGE"]   = EDGE2;
  element_equivalence_map["EDGE2"]  = EDGE2;
  element_equivalence_map["TRUSS"]  = EDGE2;
  element_equivalence_map["BEAM"]   = EDGE2;
  element_equivalence_map["BAR"]    = EDGE2;
  element_equivalence_map["TRUSS2"] = EDGE2;
  element_equivalence_map["BEAM2"]  = EDGE2;
  element_equivalence_map["BAR2"]   = EDGE2;

  // EDGE3 equivalences
  element_equivalence_map["EDGE3"]  = EDGE3;
  element_equivalence_map["TRUSS3"] = EDGE3;
  element_equivalence_map["BEAM3"]  = EDGE3;
  element_equivalence_map["BAR3"]   = EDGE3;

  // QUAD4 equivalences
  element_equivalence_map["QUAD"]   = QUAD4;
  element_equivalence_map["QUAD4"]  = QUAD4;

  // QUADSHELL4 equivalences
  element_equivalence_map["SHELL"]  = QUADSHELL4;
  element_equivalence_map["SHELL4"] = QUADSHELL4;

  // QUAD8 equivalences
  element_equivalence_map["QUAD8"]  = QUAD8;

  // QUADSHELL8 equivalences
  element_equivalence_map["SHELL8"] = QUADSHELL8;

  // QUAD9 equivalences
  element_equivalence_map["QUAD9"]  = QUAD9;
  // element_equivalence_map["SHELL9"] = QUAD9;

  // TRI3 equivalences
  element_equivalence_map["TRI"]       = TRI3;
  element_equivalence_map["TRI3"]      = TRI3;
  element_equivalence_map["TRIANGLE"]  = TRI3;

  // TRISHELL3 equivalences
  element_equivalence_map["TRISHELL"]  = TRISHELL3;
  element_equivalence_map["TRISHELL3"] = TRISHELL3;

  // TRI6 equivalences
  element_equivalence_map["TRI6"]      = TRI6;
  // element_equivalence_map["TRISHELL6"] = TRI6;

  // HEX8 equivalences
  element_equivalence_map["HEX"]  = HEX8;
  element_equivalence_map["HEX8"] = HEX8;

  // HEX20 equivalences
  element_equivalence_map["HEX20"] = HEX20;

  // HEX27 equivalences
  element_equivalence_map["HEX27"] = HEX27;

  // TET4 equivalences
  element_equivalence_map["TETRA"]  = TET4;
  element_equivalence_map["TETRA4"] = TET4;

  // TET10 equivalences
  element_equivalence_map["TETRA10"] = TET10;

  // PRISM6 equivalences
  element_equivalence_map["WEDGE"] = PRISM6;

  // PRISM15 equivalences
  element_equivalence_map["WEDGE15"] = PRISM15;

  // PRISM18 equivalences
  element_equivalence_map["WEDGE18"] = PRISM18;

  // PYRAMID5 equivalences
  element_equivalence_map["PYRAMID"]  = PYRAMID5;
  element_equivalence_map["PYRAMID5"] = PYRAMID5;

  // PYRAMID13 equivalences
  element_equivalence_map["PYRAMID13"] = PYRAMID13;

  // PYRAMID14 equivalences
  element_equivalence_map["PYRAMID14"] = PYRAMID14;
}

const ExodusII_IO_Helper::Conversion &
ExodusII_IO_Helper::get_conversion(const ElemType type) const
{
  return libmesh_map_find(conversion_map, type);
}

const ExodusII_IO_Helper::Conversion &
ExodusII_IO_Helper::get_conversion(std::string type_str) const
{
  // Do only upper-case comparisons
  std::transform(type_str.begin(), type_str.end(), type_str.begin(), ::toupper);
  return get_conversion (libmesh_map_find(element_equivalence_map, type_str));
}

const char * ExodusII_IO_Helper::get_elem_type() const
{
  return elem_type.data();
}



void ExodusII_IO_Helper::message(const std::string & msg)
{
  if (verbose) libMesh::out << msg << std::endl;
}



void ExodusII_IO_Helper::message(const std::string & msg, int i)
{
  if (verbose) libMesh::out << msg << i << "." << std::endl;
}


ExodusII_IO_Helper::MappedOutputVector::
MappedOutputVector(const std::vector<Real> & our_data_in,
                   bool single_precision_in)
  : our_data(our_data_in),
    single_precision(single_precision_in)
{
  if (single_precision)
    {
      if (sizeof(Real) != sizeof(float))
        float_vec.assign(our_data.begin(), our_data.end());
    }

  else if (sizeof(Real) != sizeof(double))
    double_vec.assign(our_data.begin(), our_data.end());
}

void *
ExodusII_IO_Helper::MappedOutputVector::data()
{
  if (single_precision)
    {
      if (sizeof(Real) != sizeof(float))
        return static_cast<void*>(float_vec.data());
    }

  else if (sizeof(Real) != sizeof(double))
    return static_cast<void*>(double_vec.data());

  // Otherwise return a (suitably casted) pointer to the original underlying data.
  return const_cast<void *>(static_cast<const void *>(our_data.data()));
}

ExodusII_IO_Helper::MappedInputVector::
MappedInputVector(std::vector<Real> & our_data_in,
                  bool single_precision_in)
  : our_data(our_data_in),
    single_precision(single_precision_in)
{
  // Allocate temporary space to store enough floats/doubles, if required.
  if (single_precision)
    {
      if (sizeof(Real) != sizeof(float))
        float_vec.resize(our_data.size());
    }
  else if (sizeof(Real) != sizeof(double))
    double_vec.resize(our_data.size());
}

ExodusII_IO_Helper::MappedInputVector::
~MappedInputVector()
{
  if (single_precision)
    {
      if (sizeof(Real) != sizeof(float))
        our_data.assign(float_vec.begin(), float_vec.end());
    }
  else if (sizeof(Real) != sizeof(double))
    our_data.assign(double_vec.begin(), double_vec.end());
}

void *
ExodusII_IO_Helper::MappedInputVector::data()
{
  if (single_precision)
    {
      if (sizeof(Real) != sizeof(float))
        return static_cast<void*>(float_vec.data());
    }

  else if (sizeof(Real) != sizeof(double))
    return static_cast<void*>(double_vec.data());

  // Otherwise return a (suitably casted) pointer to the original underlying data.
  return static_cast<void *>(our_data.data());
}

void ExodusII_IO_Helper::open(const char * filename, bool read_only)
{
  // Version of Exodus you are using
  float ex_version = 0.;

  int comp_ws = 0;

  if (_single_precision)
    comp_ws = cast_int<int>(sizeof(float));

  // Fall back on double precision when necessary since ExodusII
  // doesn't seem to support long double
  else
    comp_ws = cast_int<int>(std::min(sizeof(Real), sizeof(double)));

  // Word size in bytes of the floating point data as they are stored
  // in the ExodusII file.  "If this argument is 0, the word size of the
  // floating point data already stored in the file is returned"
  int io_ws = 0;

  ex_id = exII::ex_open(filename,
                        read_only ? EX_READ : EX_WRITE,
                        &comp_ws,
                        &io_ws,
                        &ex_version);

  std::string err_msg = std::string("Error opening ExodusII mesh file: ") + std::string(filename);
  EX_CHECK_ERR(ex_id, err_msg);
  if (verbose) libMesh::out << "File opened successfully." << std::endl;

  if (read_only)
    opened_for_reading = true;
  else
    opened_for_writing = true;

  current_filename = std::string(filename);
}



ExodusHeaderInfo
ExodusII_IO_Helper::read_header() const
{
  // Read init params using newer API that reads into a struct.  For
  // backwards compatibility, assign local member values from struct
  // afterwards. Note: using the new API allows us to automatically
  // read edge and face block/set information if it's present in the
  // file.
  exII::ex_init_params params = {};
  int err_flag = exII::ex_get_init_ext(ex_id, &params);
  EX_CHECK_ERR(err_flag, "Error retrieving header info.");

  // Extract required data into our struct
  ExodusHeaderInfo h;
  h.title.assign(params.title, params.title + MAX_LINE_LENGTH);
  h.num_dim = params.num_dim;
  h.num_nodes = params.num_nodes;
  h.num_elem = params.num_elem;
  h.num_elem_blk = params.num_elem_blk;
  h.num_node_sets = params.num_node_sets;
  h.num_side_sets = params.num_side_sets;
  h.num_edge_blk = params.num_edge_blk;
  h.num_edge = params.num_edge;

  // And return it
  return h;
}



void ExodusII_IO_Helper::read_and_store_header_info()
{
  // Read header params from file, storing them in this class's
  // ExodusHeaderInfo struct.  This automatically updates the local
  // num_dim, num_elem, etc. referenes.
  this->header_info = this->read_header();

  // Read the number of timesteps which are present in the file
  this->read_num_time_steps();

  ex_error = exII::ex_get_var_param(ex_id, "n", &num_nodal_vars);
  EX_CHECK_ERR(ex_error, "Error reading number of nodal variables.");

  ex_error = exII::ex_get_var_param(ex_id, "e", &num_elem_vars);
  EX_CHECK_ERR(ex_error, "Error reading number of elemental variables.");

  ex_error = exII::ex_get_var_param(ex_id, "g", &num_global_vars);
  EX_CHECK_ERR(ex_error, "Error reading number of global variables.");

  ex_error = exII::ex_get_var_param(ex_id, "s", &num_sideset_vars);
  EX_CHECK_ERR(ex_error, "Error reading number of sideset variables.");

  message("Exodus header info retrieved successfully.");
}




void ExodusII_IO_Helper::read_qa_records()
{
  // The QA records are four MAX_STR_LENGTH-byte character strings.
  int num_qa_rec =
    inquire(*this, exII::EX_INQ_QA, "Error retrieving number of QA records");

  if (verbose)
    libMesh::out << "Found "
                 << num_qa_rec
                 << " QA record(s) in the Exodus file."
                 << std::endl;

  if (num_qa_rec > 0)
    {
      // How to dynamically allocate an array of fixed-size char * arrays in C++.
      // http://stackoverflow.com/questions/8529359/creating-a-dynamic-sized-array-of-fixed-sized-int-arrays-in-c
      typedef char * inner_array_t[4];
      inner_array_t * qa_record = new inner_array_t[num_qa_rec];

      for (int i=0; i<num_qa_rec; i++)
        for (int j=0; j<4; j++)
          qa_record[i][j] = new char[MAX_STR_LENGTH+1];

      ex_error = exII::ex_get_qa (ex_id, qa_record);
      EX_CHECK_ERR(ex_error, "Error reading the QA records.");

      // Print the QA records
      if (verbose)
        {
          for (int i=0; i<num_qa_rec; i++)
            {
              libMesh::out << "QA Record: " << i << std::endl;
              for (int j=0; j<4; j++)
                libMesh::out << qa_record[i][j] << std::endl;
            }
        }


      // Clean up dynamically-allocated memory
      for (int i=0; i<num_qa_rec; i++)
        for (int j=0; j<4; j++)
          delete [] qa_record[i][j];

      delete [] qa_record;
    }
}




void ExodusII_IO_Helper::print_header()
{
  if (verbose)
    libMesh::out << "Title: \t" << title.data() << std::endl
                 << "Mesh Dimension: \t"   << num_dim << std::endl
                 << "Number of Nodes: \t" << num_nodes << std::endl
                 << "Number of elements: \t" << num_elem << std::endl
                 << "Number of elt blocks: \t" << num_elem_blk << std::endl
                 << "Number of node sets: \t" << num_node_sets << std::endl
                 << "Number of side sets: \t" << num_side_sets << std::endl;
}



void ExodusII_IO_Helper::read_nodes()
{
  x.resize(num_nodes);
  y.resize(num_nodes);
  z.resize(num_nodes);

  if (num_nodes)
    {
      ex_error = exII::ex_get_coord
        (ex_id,
         MappedInputVector(x, _single_precision).data(),
         MappedInputVector(y, _single_precision).data(),
         MappedInputVector(z, _single_precision).data());

      EX_CHECK_ERR(ex_error, "Error retrieving nodal data.");
      message("Nodal data retrieved successfully.");
    }
}



void ExodusII_IO_Helper::read_node_num_map ()
{
  node_num_map.resize(num_nodes);

  // Note: we cannot use the exII::ex_get_num_map() here because it
  // (apparently) does not behave like ex_get_node_num_map() when
  // there is no node number map in the file: it throws an error
  // instead of returning a default identity array (1,2,3,...).
  ex_error = exII::ex_get_node_num_map
    (ex_id, node_num_map.empty() ? nullptr : node_num_map.data());

  EX_CHECK_ERR(ex_error, "Error retrieving nodal number map.");
  message("Nodal numbering map retrieved successfully.");

  if (verbose)
    {
      libMesh::out << "[" << this->processor_id() << "] node_num_map[i] = ";
      for (unsigned int i=0; i<static_cast<unsigned int>(std::min(10, num_nodes-1)); ++i)
        libMesh::out << node_num_map[i] << ", ";
      libMesh::out << "... " << node_num_map.back() << std::endl;
    }
}


void ExodusII_IO_Helper::print_nodes(std::ostream & out_stream)
{
  for (int i=0; i<num_nodes; i++)
    out_stream << "(" << x[i] << ", " << y[i] << ", " << z[i] << ")" << std::endl;
}



void ExodusII_IO_Helper::read_block_info()
{
  if (num_elem_blk)
    {
      // Read all element block IDs.
      block_ids.resize(num_elem_blk);
      ex_error = exII::ex_get_ids(ex_id,
                                exII::EX_ELEM_BLOCK,
                                block_ids.data());

      EX_CHECK_ERR(ex_error, "Error getting block IDs.");
      message("All block IDs retrieved successfully.");

      char name_buffer[MAX_STR_LENGTH+1];
      for (int i=0; i<num_elem_blk; ++i)
        {
          ex_error = exII::ex_get_name(ex_id, exII::EX_ELEM_BLOCK,
                                     block_ids[i], name_buffer);
          EX_CHECK_ERR(ex_error, "Error getting block name.");
          id_to_block_names[block_ids[i]] = name_buffer;
        }
      message("All block names retrieved successfully.");
    }

  if (num_edge_blk)
    {
      // Read all edge block IDs.
      edge_block_ids.resize(num_edge_blk);
      ex_error = exII::ex_get_ids(ex_id,
                                exII::EX_EDGE_BLOCK,
                                edge_block_ids.data());

      EX_CHECK_ERR(ex_error, "Error getting edge block IDs.");
      message("All edge block IDs retrieved successfully.");

      // Read in edge block names
      char name_buffer[MAX_STR_LENGTH+1];
      for (int i=0; i<num_edge_blk; ++i)
        {
          ex_error = exII::ex_get_name(ex_id, exII::EX_EDGE_BLOCK,
                                     edge_block_ids[i], name_buffer);
          EX_CHECK_ERR(ex_error, "Error getting block name.");
          id_to_edge_block_names[edge_block_ids[i]] = name_buffer;
        }
      message("All edge block names retrieved successfully.");
    }
}



int ExodusII_IO_Helper::get_block_id(int index)
{
  libmesh_assert_less (index, block_ids.size());

  return block_ids[index];
}



std::string ExodusII_IO_Helper::get_block_name(int index)
{
  libmesh_assert_less (index, block_ids.size());

  return id_to_block_names[block_ids[index]];
}



int ExodusII_IO_Helper::get_side_set_id(int index)
{
  libmesh_assert_less (index, ss_ids.size());

  return ss_ids[index];
}



std::string ExodusII_IO_Helper::get_side_set_name(int index)
{
  libmesh_assert_less (index, ss_ids.size());

  return id_to_ss_names[ss_ids[index]];
}



int ExodusII_IO_Helper::get_node_set_id(int index)
{
  libmesh_assert_less (index, nodeset_ids.size());

  return nodeset_ids[index];
}



std::string ExodusII_IO_Helper::get_node_set_name(int index)
{
  libmesh_assert_less (index, nodeset_ids.size());

  return id_to_ns_names[nodeset_ids[index]];
}




void ExodusII_IO_Helper::read_elem_in_block(int block)
{
  libmesh_assert_less (block, block_ids.size());

  // Unlike the other "extended" APIs, this one does not use a parameter struct.
  int num_edges_per_elem = 0;
  int num_faces_per_elem = 0;
  ex_error = exII::ex_get_block(ex_id,
                              exII::EX_ELEM_BLOCK,
                              block_ids[block],
                              elem_type.data(),
                              &num_elem_this_blk,
                              &num_nodes_per_elem,
                              &num_edges_per_elem, // 0 or -1 if no "extended" block info
                              &num_faces_per_elem, // 0 or -1 if no "extended" block info
                              &num_attr);

  EX_CHECK_ERR(ex_error, "Error getting block info.");
  message("Info retrieved successfully for block: ", block);

  // Warn that we don't currently support reading blocks with extended info.
  // Note: the docs say -1 will be returned for this but I found that it was
  // actually 0, so not sure which it will be in general.
  if (!(num_edges_per_elem == 0) && !(num_edges_per_elem == -1))
    libmesh_warning("Exodus files with extended edge connectivity not currently supported.");
  if (!(num_faces_per_elem == 0) && !(num_faces_per_elem == -1))
    libmesh_warning("Exodus files with extended face connectivity not currently supported.");

  if (verbose)
    libMesh::out << "Read a block of " << num_elem_this_blk
                 << " " << elem_type.data() << "(s)"
                 << " having " << num_nodes_per_elem
                 << " nodes per element." << std::endl;

  // Read in the connectivity of the elements of this block,
  // watching out for the case where we actually have no
  // elements in this block (possible with parallel files)
  connect.resize(num_nodes_per_elem*num_elem_this_blk);

  if (!connect.empty())
    {
      ex_error = exII::ex_get_conn(ex_id,
                                 exII::EX_ELEM_BLOCK,
                                 block_ids[block],
                                 connect.data(), // node_conn
                                 nullptr,        // elem_edge_conn (unused)
                                 nullptr);       // elem_face_conn (unused)

      EX_CHECK_ERR(ex_error, "Error reading block connectivity.");
      message("Connectivity retrieved successfully for block: ", block);
    }
}



void ExodusII_IO_Helper::read_edge_blocks(MeshBase & mesh)
{
  // Check for quick return if there are no edge blocks.
  if (num_edge_blk == 0)
    return;

  // Build data structure that we can quickly search for edges
  // and then add required BoundaryInfo information. This is a
  // map from edge->key() to a list of (elem_id, edge_id) pairs
  // for the Edge in question. Since edge->key() is edge orientation
  // invariant, this map does not distinguish different orientations
  // of the same Edge.
  typedef std::pair<dof_id_type, unsigned int> ElemEdgePair;
  std::unordered_map<dof_id_type, std::vector<ElemEdgePair>> edge_map;
  std::unique_ptr<Elem> edge_ptr;
  for (const auto & elem : mesh.element_ptr_range())
    for (auto e : elem->edge_index_range())
      {
        elem->build_edge_ptr(edge_ptr, e);
        dof_id_type edge_key = edge_ptr->key();

        // Creates vector if not already there
        auto & vec = edge_map[edge_key];
        vec.emplace_back(elem->id(), e);
      }

  // Get reference to the mesh's BoundaryInfo object, as we will be
  // adding edges to this below.
  BoundaryInfo & bi = mesh.get_boundary_info();

  for (const auto & edge_block_id : edge_block_ids)
    {
      // exII::ex_get_block() output parameters.  Unlike the other
      // "extended" APIs, exII::ex_get_block() does not use a
      // parameter struct.
      int num_edge_this_blk = 0;
      int num_nodes_per_edge = 0;
      int num_edges_per_edge = 0;
      int num_faces_per_edge = 0;
      int num_attr_per_edge = 0;
      ex_error = exII::ex_get_block(ex_id,
                                  exII::EX_EDGE_BLOCK,
                                  edge_block_id,
                                  elem_type.data(),
                                  &num_edge_this_blk,
                                  &num_nodes_per_edge,
                                  &num_edges_per_edge, // 0 or -1 for edge blocks
                                  &num_faces_per_edge, // 0 or -1 for edge blocks
                                  &num_attr_per_edge);

      EX_CHECK_ERR(ex_error, "Error getting edge block info.");
      message("Info retrieved successfully for block: ", edge_block_id);

      // Read in the connectivity of the edges of this block,
      // watching out for the case where we actually have no
      // elements in this block (possible with parallel files)
      connect.resize(num_nodes_per_edge * num_edge_this_blk);

      if (!connect.empty())
        {
          ex_error = exII::ex_get_conn(ex_id,
                                     exII::EX_EDGE_BLOCK,
                                     edge_block_id,
                                     connect.data(), // node_conn
                                     nullptr,        // elem_edge_conn (unused)
                                     nullptr);       // elem_face_conn (unused)

          EX_CHECK_ERR(ex_error, "Error reading block connectivity.");
          message("Connectivity retrieved successfully for block: ", edge_block_id);

          // All edge types have an identity mapping from the corresponding
          // Exodus type, so we don't need to bother with mapping ids, but
          // we do need to know what kind of elements to build.
          const auto & conv = get_conversion(std::string(elem_type.data()));

          // Loop over indices in connectivity array, build edge elements,
          // look them up in the edge_map.
          for (unsigned int i=0, sz=connect.size(); i<sz; i+=num_nodes_per_edge)
            {
              auto edge = Elem::build(conv.libmesh_elem_type());
              for (int n=0; n<num_nodes_per_edge; ++n)
                {
                  int exodus_node_id = connect[i+n];
                  int exodus_node_id_zero_based = exodus_node_id - 1;
                  int libmesh_node_id = node_num_map[exodus_node_id_zero_based] - 1;

                  edge->set_node(n) = mesh.node_ptr(libmesh_node_id);
                }

              // Compute key for the edge Elem we just built.
              dof_id_type edge_key = edge->key();

              // If this key is not found in the edge_map, which is
              // supposed to include every edge in the Mesh, then we
              // need to throw an error.
              auto & elem_edge_pair_vec =
                libmesh_map_find(edge_map, edge_key);

              for (const auto & elem_edge_pair : elem_edge_pair_vec)
                {
                  // We only want to match edges which have the same
                  // orientation (node ordering) to the one in the
                  // Exodus file, otherwise we ignore this elem_edge_pair.
                  //
                  // Note: this also handles the situation where two
                  // edges have the same key (hash collision) as then
                  // this check avoids a false positive.

                  // Build edge indicated by elem_edge_pair
                  mesh.elem_ptr(elem_edge_pair.first)->
                    build_edge_ptr(edge_ptr, elem_edge_pair.second);

                  // Determine whether this candidate edge is a "real" match,
                  // i.e. also has the same orientation.
                  bool is_match = true;
                  for (int n=0; n<num_nodes_per_edge; ++n)
                    if (edge_ptr->node_id(n) != edge->node_id(n))
                      {
                        is_match = false;
                        break;
                      }

                  if (is_match)
                    {
                      // Add this (elem, edge, id) combo to the BoundaryInfo object.
                      bi.add_edge(elem_edge_pair.first,
                                  elem_edge_pair.second,
                                  edge_block_id);
                    }
                } // end loop over elem_edge_pairs
            } // end loop over connectivity array

          // Set edgeset name in the BoundaryInfo object.
          bi.edgeset_name(edge_block_id) = id_to_edge_block_names[edge_block_id];
        } // end if !connect.empty()
    } // end for edge_block_id : edge_block_ids
}



void ExodusII_IO_Helper::read_elem_num_map ()
{
  elem_num_map.resize(num_elem);

  // Note: we cannot use the exII::ex_get_num_map() here because it
  // (apparently) does not behave like ex_get_elem_num_map() when
  // there is no elem number map in the file: it throws an error
  // instead of returning a default identity array (1,2,3,...).
  ex_error = exII::ex_get_elem_num_map
    (ex_id, elem_num_map.empty() ? nullptr : elem_num_map.data());

  EX_CHECK_ERR(ex_error, "Error retrieving element number map.");
  message("Element numbering map retrieved successfully.");


  if (verbose)
    {
      libMesh::out << "[" << this->processor_id() << "] elem_num_map[i] = ";
      for (unsigned int i=0; i<static_cast<unsigned int>(std::min(10, num_elem-1)); ++i)
        libMesh::out << elem_num_map[i] << ", ";
      libMesh::out << "... " << elem_num_map.back() << std::endl;
    }
}



void ExodusII_IO_Helper::read_sideset_info()
{
  ss_ids.resize(num_side_sets);
  if (num_side_sets > 0)
    {
      ex_error = exII::ex_get_ids(ex_id,
                                exII::EX_SIDE_SET,
                                ss_ids.data());
      EX_CHECK_ERR(ex_error, "Error retrieving sideset information.");
      message("All sideset information retrieved successfully.");

      // Resize appropriate data structures -- only do this once outside the loop
      num_sides_per_set.resize(num_side_sets);
      num_df_per_set.resize(num_side_sets);

      // Inquire about the length of the concatenated side sets element list
      num_elem_all_sidesets = inquire(*this, exII::EX_INQ_SS_ELEM_LEN, "Error retrieving length of the concatenated side sets element list!");

      elem_list.resize (num_elem_all_sidesets);
      side_list.resize (num_elem_all_sidesets);
      id_list.resize   (num_elem_all_sidesets);
    }

  char name_buffer[MAX_STR_LENGTH+1];
  for (int i=0; i<num_side_sets; ++i)
    {
      ex_error = exII::ex_get_name(ex_id, exII::EX_SIDE_SET,
                                 ss_ids[i], name_buffer);
      EX_CHECK_ERR(ex_error, "Error getting side set name.");
      id_to_ss_names[ss_ids[i]] = name_buffer;
    }
  message("All side set names retrieved successfully.");
}


void ExodusII_IO_Helper::read_nodeset_info()
{
  nodeset_ids.resize(num_node_sets);
  if (num_node_sets > 0)
    {
      ex_error = exII::ex_get_ids(ex_id,
                                exII::EX_NODE_SET,
                                nodeset_ids.data());
      EX_CHECK_ERR(ex_error, "Error retrieving nodeset information.");
      message("All nodeset information retrieved successfully.");

      // Resize appropriate data structures -- only do this once outnode the loop
      num_nodes_per_set.resize(num_node_sets);
      num_node_df_per_set.resize(num_node_sets);
    }

  char name_buffer[MAX_STR_LENGTH+1];
  for (int i=0; i<num_node_sets; ++i)
    {
      ex_error = exII::ex_get_name(ex_id, exII::EX_NODE_SET,
                                 nodeset_ids[i], name_buffer);
      EX_CHECK_ERR(ex_error, "Error getting node set name.");
      id_to_ns_names[nodeset_ids[i]] = name_buffer;
    }
  message("All node set names retrieved successfully.");
}



void ExodusII_IO_Helper::read_sideset(int id, int offset)
{
  libmesh_assert_less (id, ss_ids.size());
  libmesh_assert_less (id, num_sides_per_set.size());
  libmesh_assert_less (id, num_df_per_set.size());
  libmesh_assert_less_equal (offset, elem_list.size());
  libmesh_assert_less_equal (offset, side_list.size());

  ex_error = exII::ex_get_set_param(ex_id,
                                  exII::EX_SIDE_SET,
                                  ss_ids[id],
                                  &num_sides_per_set[id],
                                  &num_df_per_set[id]);
  EX_CHECK_ERR(ex_error, "Error retrieving sideset parameters.");
  message("Parameters retrieved successfully for sideset: ", id);


  // It's OK for offset==elem_list.size() as long as num_sides_per_set[id]==0
  // because in that case we don't actually read anything...
#ifdef DEBUG
  if (static_cast<unsigned int>(offset) == elem_list.size() ||
      static_cast<unsigned int>(offset) == side_list.size() )
    libmesh_assert_equal_to (num_sides_per_set[id], 0);
#endif


  // Don't call ex_get_set unless there are actually sides there to get.
  // Exodus prints an annoying warning in DEBUG mode otherwise...
  if (num_sides_per_set[id] > 0)
    {
      ex_error = exII::ex_get_set(ex_id,
                                exII::EX_SIDE_SET,
                                ss_ids[id],
                                &elem_list[offset],
                                &side_list[offset]);
      EX_CHECK_ERR(ex_error, "Error retrieving sideset data.");
      message("Data retrieved successfully for sideset: ", id);

      for (int i=0; i<num_sides_per_set[id]; i++)
        id_list[i+offset] = ss_ids[id];
    }
}



void ExodusII_IO_Helper::read_all_nodesets()
{
  // Figure out how many nodesets there are in the file so we can
  // properly resize storage as necessary.
  num_node_sets =
    inquire
    (*this, exII::EX_INQ_NODE_SETS,
     "Error retrieving number of node sets");

  // Figure out how many nodes there are in all the nodesets.
  int total_nodes_in_all_sets =
    inquire
    (*this, exII::EX_INQ_NS_NODE_LEN,
     "Error retrieving number of nodes in all node sets.");

  // Figure out how many distribution factors there are in all the nodesets.
  int total_df_in_all_sets =
    inquire
    (*this, exII::EX_INQ_NS_DF_LEN,
     "Error retrieving number of distribution factors in all node sets.");

  // If there are no nodesets, there's nothing to read in.
  if (num_node_sets == 0)
    return;

  // Allocate space to read all the nodeset data.
  // Use existing class members where possible to avoid shadowing
  nodeset_ids.clear();          nodeset_ids.resize(num_node_sets);
  num_nodes_per_set.clear();    num_nodes_per_set.resize(num_node_sets);
  num_node_df_per_set.clear();  num_node_df_per_set.resize(num_node_sets);
  node_sets_node_index.clear(); node_sets_node_index.resize(num_node_sets);
  node_sets_dist_index.clear(); node_sets_dist_index.resize(num_node_sets);
  node_sets_node_list.clear();  node_sets_node_list.resize(total_nodes_in_all_sets);
  node_sets_dist_fact.clear();  node_sets_dist_fact.resize(total_df_in_all_sets);

  ex_error = exII::ex_get_concat_node_sets
    (ex_id,
     nodeset_ids.data(),
     num_nodes_per_set.data(),
     num_node_df_per_set.data(),
     node_sets_node_index.data(),
     node_sets_dist_index.data(),
     node_sets_node_list.data(),
     total_df_in_all_sets ?
     MappedInputVector(node_sets_dist_fact, _single_precision).data() : nullptr);

  EX_CHECK_ERR(ex_error, "Error reading concatenated nodesets");

  // Read the nodeset names from file!
  char name_buffer[MAX_STR_LENGTH+1];
  for (int i=0; i<num_node_sets; ++i)
    {
      ex_error = exII::ex_get_name
        (ex_id,
         exII::EX_NODE_SET,
         nodeset_ids[i],
         name_buffer);
      EX_CHECK_ERR(ex_error, "Error getting node set name.");
      id_to_ns_names[nodeset_ids[i]] = name_buffer;
    }
}



void ExodusII_IO_Helper::close()
{
  // Always call close on processor 0.
  // If we're running on multiple processors, i.e. as one of several Nemesis files,
  // we call close on all processors...
  if ((this->processor_id() == 0) || (!_run_only_on_proc0))
    {
      // Don't close the file if it was never opened, this raises an Exodus error
      if (opened_for_writing || opened_for_reading)
        {
          ex_error = exII::ex_close(ex_id);
          EX_CHECK_ERR(ex_error, "Error closing Exodus file.");
          message("Exodus file closed successfully.");

          // Now that the file is closed, it's no longer opened for
          // reading or writing.
          opened_for_writing = false;
          opened_for_reading = false;
        }
    }
}



void ExodusII_IO_Helper::read_time_steps()
{
  // Make sure we have an up-to-date count of the number of time steps in the file.
  this->read_num_time_steps();

  if (num_time_steps > 0)
    {
      time_steps.resize(num_time_steps);
      ex_error = exII::ex_get_all_times
        (ex_id,
         MappedInputVector(time_steps, _single_precision).data());
      EX_CHECK_ERR(ex_error, "Error reading timesteps!");
    }
}



void ExodusII_IO_Helper::read_num_time_steps()
{
  num_time_steps =
    inquire(*this, exII::EX_INQ_TIME, "Error retrieving number of time steps");
}



void ExodusII_IO_Helper::read_nodal_var_values(std::string nodal_var_name, int time_step)
{
  // Read the nodal variable names from file, so we can see if we have the one we're looking for
  this->read_var_names(NODAL);

  // See if we can find the variable we are looking for
  unsigned int var_index = 0;
  bool found = false;

  // Do a linear search for nodal_var_name in nodal_var_names
  for (; var_index<nodal_var_names.size(); ++var_index)
    {
      found = (nodal_var_names[var_index] == nodal_var_name);
      if (found)
        break;
    }

  if (!found)
    {
      libMesh::err << "Available variables: " << std::endl;
      for (const auto & var_name : nodal_var_names)
        libMesh::err << var_name << std::endl;

      libmesh_error_msg("Unable to locate variable named: " << nodal_var_name);
    }

  // Clear out any previously read nodal variable values
  nodal_var_values.clear();

  std::vector<Real> unmapped_nodal_var_values(num_nodes);

  // Call the Exodus API to read the nodal variable values
  ex_error = exII::ex_get_nodal_var
    (ex_id,
     time_step,
     var_index+1,
     num_nodes,
     MappedInputVector(unmapped_nodal_var_values, _single_precision).data());
  EX_CHECK_ERR(ex_error, "Error reading nodal variable values!");

  for (unsigned i=0; i<static_cast<unsigned>(num_nodes); i++)
    {
      libmesh_assert_less(i, this->node_num_map.size());

      // Use the node_num_map to obtain the ID of this node in the Exodus file,
      // and remember to subtract 1 since libmesh is zero-based and Exodus is 1-based.
      const unsigned mapped_node_id = this->node_num_map[i] - 1;

      libmesh_assert_less(i, unmapped_nodal_var_values.size());

      // Store the nodal value in the map.
      nodal_var_values[mapped_node_id] = unmapped_nodal_var_values[i];
    }
}



void ExodusII_IO_Helper::read_var_names(ExodusVarType type)
{
  switch (type)
    {
    case NODAL:
      this->read_var_names_impl("n", num_nodal_vars, nodal_var_names);
      break;
    case ELEMENTAL:
      this->read_var_names_impl("e", num_elem_vars, elem_var_names);
      break;
    case GLOBAL:
      this->read_var_names_impl("g", num_global_vars, global_var_names);
      break;
    case SIDESET:
      this->read_var_names_impl("s", num_sideset_vars, sideset_var_names);
      break;
    case NODESET:
      this->read_var_names_impl("m", num_nodeset_vars, nodeset_var_names);
      break;
    default:
      libmesh_error_msg("Unrecognized ExodusVarType " << type);
    }
}



void ExodusII_IO_Helper::read_var_names_impl(const char * var_type,
                                             int & count,
                                             std::vector<std::string> & result)
{
  // First read and store the number of names we have
  ex_error = exII::ex_get_var_param(ex_id, var_type, &count);
  EX_CHECK_ERR(ex_error, "Error reading number of variables.");

  // Do nothing if no variables are detected
  if (count == 0)
    return;

  // Second read the actual names and convert them into a format we can use
  NamesData names_table(count, MAX_STR_LENGTH);

  ex_error = exII::ex_get_var_names(ex_id,
                                  var_type,
                                  count,
                                  names_table.get_char_star_star()
                                  );
  EX_CHECK_ERR(ex_error, "Error reading variable names!");

  if (verbose)
    {
      libMesh::out << "Read the variable(s) from the file:" << std::endl;
      for (int i=0; i<count; i++)
        libMesh::out << names_table.get_char_star(i) << std::endl;
    }

  // Allocate enough space for our variable name strings.
  result.resize(count);

  // Copy the char buffers into strings.
  for (int i=0; i<count; i++)
    result[i] = names_table.get_char_star(i); // calls string::op=(const char *)
}




void
ExodusII_IO_Helper::write_var_names(ExodusVarType type,
                                    const std::vector<std::string> & names)
{
  switch (type)
    {
    case NODAL:
      this->write_var_names_impl("n", num_nodal_vars, names);
      break;
    case ELEMENTAL:
      this->write_var_names_impl("e", num_elem_vars, names);
      break;
    case GLOBAL:
      this->write_var_names_impl("g", num_global_vars, names);
      break;
    case SIDESET:
      {
        // Note: calling this function *sets* num_sideset_vars to the
        // number of entries in the 'names' vector, num_sideset_vars
        // does not already need to be set before calling this.
        this->write_var_names_impl("s", num_sideset_vars, names);
        break;
      }
    case NODESET:
      {
        this->write_var_names_impl("m", num_nodeset_vars, names);
        break;
      }
    default:
      libmesh_error_msg("Unrecognized ExodusVarType " << type);
    }
}



void
ExodusII_IO_Helper::write_var_names_impl(const char * var_type,
                                         int & count,
                                         const std::vector<std::string> & names)
{
  // Update the count variable so that it's available to other parts of the class.
  count = cast_int<int>(names.size());

  // Write that number of variables to the file.
  ex_error = exII::ex_put_var_param(ex_id, var_type, count);
  EX_CHECK_ERR(ex_error, "Error setting number of vars.");

  // Nemesis doesn't like trying to write nodal variable names in
  // files with no nodes.
  if (!this->num_nodes)
    return;

  if (count > 0)
    {
      NamesData names_table(count, MAX_STR_LENGTH);

      // Store the input names in the format required by Exodus.
      for (int i=0; i != count; ++i)
        names_table.push_back_entry(names[i]);

      if (verbose)
        {
          libMesh::out << "Writing variable name(s) to file: " << std::endl;
          for (int i=0; i != count; ++i)
            libMesh::out << names_table.get_char_star(i) << std::endl;
        }

      ex_error = exII::ex_put_var_names(ex_id,
                                      var_type,
                                      count,
                                      names_table.get_char_star_star()
                                      );

      EX_CHECK_ERR(ex_error, "Error writing variable names.");
    }
}



void ExodusII_IO_Helper::read_elemental_var_values(std::string elemental_var_name,
                                                   int time_step,
                                                   std::map<dof_id_type, Real> & elem_var_value_map)
{
  this->read_var_names(ELEMENTAL);

  // See if we can find the variable we are looking for
  unsigned int var_index = 0;
  bool found = false;

  // Do a linear search for elem_var_name in elemental_var_names
  for (; var_index != elem_var_names.size(); ++var_index)
    if (elem_var_names[var_index] == elemental_var_name)
      {
        found = true;
        break;
      }

  if (!found)
    {
      libMesh::err << "Available variables: " << std::endl;
      for (const auto & var_name : elem_var_names)
        libMesh::err << var_name << std::endl;

      libmesh_error_msg("Unable to locate variable named: " << elemental_var_name);
    }

  // Sequential index which we can use to look up the element ID in the elem_num_map.
  unsigned ex_el_num = 0;

  // Element variable truth table
  std::vector<int> var_table(block_ids.size() * elem_var_names.size());
  exII::ex_get_var_tab(ex_id, "e", block_ids.size(), elem_var_names.size(), var_table.data());

  for (unsigned i=0; i<static_cast<unsigned>(num_elem_blk); i++)
    {
      ex_error = exII::ex_get_elem_block(ex_id,
                                       block_ids[i],
                                       nullptr,
                                       &num_elem_this_blk,
                                       nullptr,
                                       nullptr);
      EX_CHECK_ERR(ex_error, "Error getting number of elements in block.");

      // If the current variable isn't active on this subdomain, advance
      // the index by the number of elements on this block and go to the
      // next loop iteration.
      if (!var_table[elem_var_names.size()*i + var_index])
        {
          ex_el_num += num_elem_this_blk;
          continue;
        }

      std::vector<Real> block_elem_var_values(num_elem_this_blk);

      ex_error = exII::ex_get_elem_var
        (ex_id,
         time_step,
         var_index+1,
         block_ids[i],
         num_elem_this_blk,
         MappedInputVector(block_elem_var_values, _single_precision).data());
      EX_CHECK_ERR(ex_error, "Error getting elemental values.");

      for (unsigned j=0; j<static_cast<unsigned>(num_elem_this_blk); j++)
        {
          // Use the elem_num_map to obtain the ID of this element in the Exodus file,
          // and remember to subtract 1 since libmesh is zero-based and Exodus is 1-based.
          unsigned mapped_elem_id = this->elem_num_map[ex_el_num] - 1;

          // Store the elemental value in the map.
          elem_var_value_map[mapped_elem_id] = block_elem_var_values[j];

          // Go to the next sequential element ID.
          ex_el_num++;
        }
    }
}


// For Writing Solutions

void ExodusII_IO_Helper::create(std::string filename)
{
  // If we're processor 0, always create the file.
  // If we running on all procs, e.g. as one of several Nemesis files, also
  // call create there.
  if ((this->processor_id() == 0) || (!_run_only_on_proc0))
    {
      int
        comp_ws = 0,
        io_ws = 0;

      if (_single_precision)
        {
          comp_ws = cast_int<int>(sizeof(float));
          io_ws = cast_int<int>(sizeof(float));
        }
      // Fall back on double precision when necessary since ExodusII
      // doesn't seem to support long double
      else
        {
          comp_ws = cast_int<int>
            (std::min(sizeof(Real), sizeof(double)));
          io_ws = cast_int<int>
            (std::min(sizeof(Real), sizeof(double)));
        }

      // By default we just open the Exodus file in "EX_CLOBBER" mode,
      // which, according to "ncdump -k", writes the file in "64-bit
      // offset" mode, which is a NETCDF3 file format.
      int mode = EX_CLOBBER;

      // If HDF5 is available, by default we will write Exodus files
      // in a more modern NETCDF4-compatible format. For this file
      // type, "ncdump -k" will report "netCDF-4".
#ifdef LIBMESH_HAVE_HDF5
      mode |= EX_NETCDF4;
      mode |= EX_NOCLASSIC;
#endif

      ex_id = exII::ex_create(filename.c_str(), mode, &comp_ws, &io_ws);

      EX_CHECK_ERR(ex_id, "Error creating ExodusII/Nemesis mesh file.");

      if (verbose)
        libMesh::out << "File created successfully." << std::endl;
    }

  opened_for_writing = true;
  current_filename = filename;
}



void ExodusII_IO_Helper::initialize(std::string str_title, const MeshBase & mesh, bool use_discontinuous)
{
  // The majority of this function only executes on processor 0, so any functions
  // which are collective, like n_active_elem() or n_edge_conds() must be called
  // before the processors' execution paths diverge.
  unsigned int n_active_elem = mesh.n_active_elem();
  const BoundaryInfo & bi = mesh.get_boundary_info();
  num_edge = bi.n_edge_conds();

  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // If _write_as_dimension is nonzero, use it to set num_dim in the Exodus file.
  if (_write_as_dimension)
    num_dim = _write_as_dimension;
  else if (_use_mesh_dimension_instead_of_spatial_dimension)
    num_dim = mesh.mesh_dimension();
  else
    num_dim = mesh.spatial_dimension();

  num_elem = mesh.n_elem();

  if (!use_discontinuous)
    {
      // Don't rely on mesh.n_nodes() here.  If nodes have been
      // deleted recently, it will be incorrect.
      num_nodes = cast_int<int>(std::distance(mesh.nodes_begin(),
                                              mesh.nodes_end()));
    }
  else
    {
      for (const auto & elem : mesh.active_element_ptr_range())
        num_nodes += elem->n_nodes();
    }

  std::vector<boundary_id_type> unique_side_boundaries;
  std::vector<boundary_id_type> unique_node_boundaries;

  bi.build_side_boundary_ids(unique_side_boundaries);
  {
    // Add shell face boundaries to the list of side boundaries, since ExodusII
    // treats these the same way.
    std::vector<boundary_id_type> shellface_boundaries;
    bi.build_shellface_boundary_ids(shellface_boundaries);
    for (const auto & id : shellface_boundaries)
      unique_side_boundaries.push_back(id);
  }
  // Add any empty-but-named side boundary ids
  for (const auto & pair : bi.get_sideset_name_map())
    {
      const boundary_id_type id = pair.first;

      if (std::find(unique_side_boundaries.begin(),
                    unique_side_boundaries.end(), id)
            == unique_side_boundaries.end())
        unique_side_boundaries.push_back(id);
    }

  bi.build_node_boundary_ids(unique_node_boundaries);
  for (const auto & pair : bi.get_nodeset_name_map())
    {
      const boundary_id_type id = pair.first;

      if (std::find(unique_node_boundaries.begin(),
                    unique_node_boundaries.end(), id)
            == unique_node_boundaries.end())
        unique_node_boundaries.push_back(id);
    }

  num_side_sets = cast_int<int>(unique_side_boundaries.size());
  num_node_sets = cast_int<int>(unique_node_boundaries.size());

  //loop through element and map between block and element vector
  std::map<subdomain_id_type, std::vector<unsigned int>> subdomain_map;

  for (const auto & elem : mesh.active_element_ptr_range())
    {
      // We skip writing infinite elements to the Exodus file, so
      // don't put them in the subdomain_map. That way the number of
      // blocks should be correct.
#ifdef LIBMESH_ENABLE_INFINITE_ELEMENTS
      if (elem->infinite())
        continue;
#endif

      subdomain_id_type cur_subdomain = elem->subdomain_id();
      subdomain_map[cur_subdomain].push_back(elem->id());
    }
  num_elem_blk = cast_int<int>(subdomain_map.size());

  if (str_title.size() > MAX_LINE_LENGTH)
    {
      libMesh::err << "Warning, Exodus files cannot have titles longer than "
                   << MAX_LINE_LENGTH
                   << " characters.  Your title will be truncated."
                   << std::endl;
      str_title.resize(MAX_LINE_LENGTH);
    }

  // Edge BCs are handled a bit differently than sidesets and nodesets.
  // They are written as separate "edge blocks", and then edge variables
  // can be defined on those blocks. That is, they are not written as
  // edge sets, since edge sets must refer to edges stored elsewhere.
  // We write a separate edge block for each unique boundary id that
  // we have.
  num_edge_blk = bi.get_edge_boundary_ids().size();

  // Build an ex_init_params() structure that is to be passed to the
  // newer ex_put_init_ext() API. The new API will eventually allow us
  // to store edge and face data in the Exodus file.
  //
  // Notes:
  // * We use C++11 zero initialization syntax to make sure that all
  //   members of the struct (including ones we aren't using) are
  //   given sensible values.
  // * For the "title" field, we manually do a null-terminated string
  //   copy since std::string does not null-terminate but it does
  //   return the number of characters successfully copied.
  exII::ex_init_params params = {};
  params.title[str_title.copy(params.title, MAX_LINE_LENGTH)] = '\0';
  params.num_dim = num_dim;
  params.num_nodes = num_nodes;
  params.num_elem = n_active_elem;
  params.num_elem_blk = num_elem_blk;
  params.num_node_sets = num_node_sets;
  params.num_side_sets = num_side_sets;
  params.num_edge_blk = num_edge_blk;
  params.num_edge = num_edge;

  ex_error = exII::ex_put_init_ext(ex_id, &params);
  EX_CHECK_ERR(ex_error, "Error initializing new Exodus file.");
}



void ExodusII_IO_Helper::write_nodal_coordinates(const MeshBase & mesh, bool use_discontinuous)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // Clear existing data from any previous calls.
  x.clear();
  y.clear();
  z.clear();
  node_num_map.clear();

  // Reserve space in the nodal coordinate vectors.  num_nodes is
  // exact, this just allows us to do away with one potentially
  // error-inducing loop index.
  x.reserve(num_nodes);
  y.reserve(num_nodes);
  z.reserve(num_nodes);

  // And in the node_num_map - since the nodes aren't organized in
  // blocks, libmesh will always write out the identity map
  // here... unless there has been some refinement and coarsening, or
  // node deletion without a corresponding call to contract(). You
  // need to write this any time there could be 'holes' in the node
  // numbering, so we write it every time.
  node_num_map.reserve(num_nodes);

  // Clear out any previously-mapped node IDs.
  libmesh_node_num_to_exodus.clear();

  if (!use_discontinuous)
    {
      for (const auto & node_ptr : mesh.node_ptr_range())
        {
          const Node & node = *node_ptr;

          x.push_back(node(0) + _coordinate_offset(0));

#if LIBMESH_DIM > 1
          y.push_back(node(1) + _coordinate_offset(1));
#else
          y.push_back(0.);
#endif
#if LIBMESH_DIM > 2
          z.push_back(node(2) + _coordinate_offset(2));
#else
          z.push_back(0.);
#endif

          // Fill in node_num_map entry with the proper (1-based) node id
          node_num_map.push_back(node.id() + 1);

          // Also map the zero-based libmesh node id to the 1-based
          // Exodus ID it will be assigned (this is equivalent to the
          // current size of the x vector).
          libmesh_node_num_to_exodus[ cast_int<int>(node.id()) ] = cast_int<int>(x.size());
        }
    }
  else
    {
      for (const auto & elem : mesh.active_element_ptr_range())
        for (const Node & node : elem->node_ref_range())
          {
            x.push_back(node(0));
#if LIBMESH_DIM > 1
            y.push_back(node(1));
#else
            y.push_back(0.);
#endif
#if LIBMESH_DIM > 2
            z.push_back(node(2));
#else
            z.push_back(0.);
#endif

            // Let's skip the node_num_map in the discontinuous
            // case, since we're effectively duplicating nodes for
            // the sake of discontinuous visualization, so it isn't
            // clear how to deal with node_num_map here. This means
            // that writing discontinuous meshes won't work with
            // element numberings that have "holes".
          }
    }

  ex_error = exII::ex_put_coord
    (ex_id,
     x.empty() ? nullptr : MappedOutputVector(x, _single_precision).data(),
     y.empty() ? nullptr : MappedOutputVector(y, _single_precision).data(),
     z.empty() ? nullptr : MappedOutputVector(z, _single_precision).data());

  EX_CHECK_ERR(ex_error, "Error writing coordinates to Exodus file.");

  if (!use_discontinuous)
    {
      // Also write the (1-based) node_num_map to the file.
      ex_error = exII::ex_put_node_num_map(ex_id, node_num_map.data());
      EX_CHECK_ERR(ex_error, "Error writing node_num_map");
    }
}



void ExodusII_IO_Helper::write_elements(const MeshBase & mesh, bool use_discontinuous)
{
  // n_active_elem() is a parallel_only function
  unsigned int n_active_elem = mesh.n_active_elem();

  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // Map from block ID to a vector of element IDs in that block.  Element
  // IDs are now of type dof_id_type, subdomain IDs are of type subdomain_id_type.
  typedef std::map<subdomain_id_type, std::vector<dof_id_type>> subdomain_map_type;
  subdomain_map_type subdomain_map;

  // Loop through element and map between block and element vector.
  for (const auto & elem : mesh.active_element_ptr_range())
    {
      // We skip writing infinite elements to the Exodus file, so
      // don't put them in the subdomain_map. That way the number of
      // blocks should be correct.
#ifdef LIBMESH_ENABLE_INFINITE_ELEMENTS
      if (elem->infinite())
        continue;
#endif

      subdomain_map[ elem->subdomain_id() ].push_back(elem->id());
    }

  // element map vector
  num_elem_blk = cast_int<int>(subdomain_map.size());
  block_ids.resize(num_elem_blk);
  elem_num_map.resize(n_active_elem);
  std::vector<int>::iterator curr_elem_map_end = elem_num_map.begin();

  std::vector<int> elem_blk_id;
  std::vector<int> num_elem_this_blk_vec;
  std::vector<int> num_nodes_per_elem_vec;
  std::vector<int> num_edges_per_elem_vec;
  std::vector<int> num_faces_per_elem_vec;
  std::vector<int> num_attr_vec;
  NamesData elem_type_table(num_elem_blk, MAX_STR_LENGTH);

  // Note: It appears that there is a bug in exodusII::ex_put_name where
  // the index returned from the ex_id_lkup is erroneously used.  For now
  // the work around is to use the alternative function ex_put_names, but
  // this function requires a char ** data structure.
  NamesData names_table(num_elem_blk, MAX_STR_LENGTH);

  // counter indexes into the block_ids vector
  unsigned int counter = 0;
  for (auto & pr : subdomain_map)
    {
      block_ids[counter] = pr.first;
      names_table.push_back_entry(mesh.subdomain_name(pr.first));

      // Get a reference to a vector of element IDs for this subdomain.
      subdomain_map_type::mapped_type & tmp_vec = pr.second;

      // Use the first element in this block to get representative information.
      // Note that Exodus assumes all elements in a block are of the same type!
      // We are using that same assumption here!
      const auto & conv = get_conversion(mesh.elem_ref(tmp_vec[0]).type());
      num_nodes_per_elem = mesh.elem_ref(tmp_vec[0]).n_nodes();

      elem_blk_id.push_back(pr.first);
      elem_type_table.push_back_entry(conv.exodus_elem_type().c_str());
      num_elem_this_blk_vec.push_back(cast_int<int>(tmp_vec.size()));
      num_nodes_per_elem_vec.push_back(num_nodes_per_elem);
      num_attr_vec.push_back(0); // we don't currently use elem block attributes.
      num_edges_per_elem_vec.push_back(0); // We don't currently store any edge blocks
      num_faces_per_elem_vec.push_back(0); // We don't currently store any face blocks
      ++counter;
    }

  // In the case of discontinuous plotting we initialize a map from
  // (element, node) pairs to the corresponding discontinuous node index.
  // This ordering must match the ordering used in write_nodal_coordinates.
  //
  // Note: This map takes the place of the libmesh_node_num_to_exodus map in
  // the discontinuous case.
  std::map<std::pair<dof_id_type, unsigned int>, dof_id_type> discontinuous_node_indices;
  if (use_discontinuous)
  {
    dof_id_type node_counter = 1; // Exodus numbering is 1-based
    for (const auto & elem : mesh.active_element_ptr_range())
      for (auto n : elem->node_index_range())
        {
          std::pair<dof_id_type,unsigned int> id_pair;
          id_pair.first = elem->id();
          id_pair.second = n;
          discontinuous_node_indices[id_pair] = node_counter;
          node_counter++;
        }
  }

  // Reference to the BoundaryInfo object for convenience.
  const BoundaryInfo & bi = mesh.get_boundary_info();

  // Build list of (elem, edge, id) triples
  std::vector<BoundaryInfo::BCTuple> edge_tuples = bi.build_edge_list();

  // Build the connectivity array for each edge block. The connectivity array
  // is a vector<int> with "num_edges * num_nodes_per_edge" entries. We write
  // the Exodus node numbers to the connectivity arrays so that they can
  // be used directly in the calls to exII::ex_put_conn() below. We also keep
  // track of the ElemType and the number of nodes for each boundary_id. All
  // edges with a given boundary_id must be of the same type.
  std::map<boundary_id_type, std::vector<int>> edge_id_to_conn;
  std::map<boundary_id_type, std::pair<ElemType, unsigned int>> edge_id_to_elem_type;

  std::unique_ptr<const Elem> edge;
  for (const auto & t : edge_tuples)
    {
      dof_id_type elem_id = std::get<0>(t);
      unsigned int edge_id = std::get<1>(t);
      boundary_id_type b_id = std::get<2>(t);

      // Build the edge in question
      mesh.elem_ptr(elem_id)->build_edge_ptr(edge, edge_id);

      // Error checking: make sure that all edges in this block are
      // the same geometric type.
      auto check_it = edge_id_to_elem_type.find(b_id);

      if (check_it == edge_id_to_elem_type.end())
        {
          // Keep track of the ElemType and number of nodes in this boundary id.
          edge_id_to_elem_type[b_id] = std::make_pair(edge->type(), edge->n_nodes());
        }
      else
        {
          // Make sure the existing data is consistent
          auto & val_pair = check_it->second;
          libmesh_error_msg_if(val_pair.first != edge->type() || val_pair.second != edge->n_nodes(),
                               "All edges in a block must have same geometric type.");
        }

      // Get reference to the connectivity array for this block
      auto & conn = edge_id_to_conn[b_id];

      // For each node on the edge, look up the exodus node id and
      // store it in the conn array. Note: all edge types have
      // identity node mappings so we don't bother with Conversion
      // objects here.
      for (auto n : edge->node_index_range())
        {
          // We look up Exodus node numbers differently if we are
          // writing a discontinuous Exodus file.
          int exodus_node_id = -1;

          if (!use_discontinuous)
            {
              dof_id_type libmesh_node_id = edge->node_ptr(n)->id();
              exodus_node_id = libmesh_map_find
                (libmesh_node_num_to_exodus, cast_int<int>(libmesh_node_id));
            }
          else
            {
              // Get the node on the element containing this edge
              // which corresponds to edge node n. Then use that id to look up
              // the exodus_node_id in the discontinuous_node_indices map.
              unsigned int pn = mesh.elem_ptr(elem_id)->local_edge_node(edge_id, n);
              exodus_node_id = libmesh_map_find
                (discontinuous_node_indices, std::make_pair(elem_id, pn));
            }

          conn.push_back(exodus_node_id);
        }
    }

  // Make sure we have the same number of edge ids that we thought we would.
  libmesh_assert(static_cast<int>(edge_id_to_conn.size()) == num_edge_blk);

  // Build data structures describing edge blocks. This information must be
  // be passed to exII::ex_put_concat_all_blocks() at the same time as the
  // information about elem blocks.
  std::vector<int> edge_blk_id;
  NamesData edge_type_table(num_edge_blk, MAX_STR_LENGTH);
  std::vector<int> num_edge_this_blk_vec;
  std::vector<int> num_nodes_per_edge_vec;
  std::vector<int> num_attr_edge_vec;

  // We also build a data structure of edge block names which can
  // later be passed to exII::ex_put_names().
  NamesData edge_block_names_table(num_edge_blk, MAX_STR_LENGTH);

  // Note: We are going to use the edge **boundary** ids as **block** ids.
  for (const auto & pr : edge_id_to_conn)
    {
      // Store the edge block id in the array to be passed to Exodus.
      boundary_id_type id = pr.first;
      edge_blk_id.push_back(id);

      // Set Exodus element type and number of nodes for this edge block.
      const auto & elem_type_node_count = edge_id_to_elem_type[id];
      const auto & conv = get_conversion(elem_type_node_count.first);
      edge_type_table.push_back_entry(conv.exodus_type.c_str());
      num_nodes_per_edge_vec.push_back(elem_type_node_count.second);

      // The number of edges is the number of entries in the connectivity
      // array divided by the number of nodes per edge.
      num_edge_this_blk_vec.push_back(pr.second.size() / elem_type_node_count.second);

      // We don't store any attributes currently
      num_attr_edge_vec.push_back(0);

      // Store the name of this edge block
      edge_block_names_table.push_back_entry(bi.get_edgeset_name(id));
    }

  // Zero-initialize and then fill in an exII::ex_block_params struct
  // with the data we have collected. This new API replaces the old
  // exII::ex_put_concat_elem_block() API, and will eventually allow
  // us to also allocate space for edge/face blocks if desired.
  //
  // TODO: It seems like we should be able to take advantage of the
  // optimization where you set define_maps==1, but when I tried this
  // I got the error: "failed to find node map size". I think the
  // problem is that we need to first specify a nonzero number of
  // node/elem maps during the call to ex_put_init_ext() in order for
  // this to work correctly.
  exII::ex_block_params params = {};

  // Set pointers for information about elem blocks.
  params.elem_blk_id = elem_blk_id.data();
  params.elem_type = elem_type_table.get_char_star_star();
  params.num_elem_this_blk = num_elem_this_blk_vec.data();
  params.num_nodes_per_elem = num_nodes_per_elem_vec.data();
  params.num_edges_per_elem = num_edges_per_elem_vec.data();
  params.num_faces_per_elem = num_faces_per_elem_vec.data();
  params.num_attr_elem = num_attr_vec.data();
  params.define_maps = 0;

  // Set pointers to edge block information only if we actually have some.
  if (num_edge_blk)
    {
      params.edge_blk_id = edge_blk_id.data();
      params.edge_type = edge_type_table.get_char_star_star();
      params.num_edge_this_blk = num_edge_this_blk_vec.data();
      params.num_nodes_per_edge = num_nodes_per_edge_vec.data();
      params.num_attr_edge = num_attr_edge_vec.data();
    }

  ex_error = exII::ex_put_concat_all_blocks(ex_id, &params);
  EX_CHECK_ERR(ex_error, "Error writing element blocks.");

  // This counter is used to fill up the libmesh_elem_num_to_exodus map in the loop below.
  unsigned libmesh_elem_num_to_exodus_counter = 0;

  for (auto & pr : subdomain_map)
    {
      // Get a reference to a vector of element IDs for this subdomain.
      subdomain_map_type::mapped_type & tmp_vec = pr.second;

      // Use the first element in this block to get representative information.
      // Note that Exodus assumes all elements in a block are of the same type!
      // We are using that same assumption here!
      const auto & conv = get_conversion(mesh.elem_ref(tmp_vec[0]).type());
      num_nodes_per_elem = mesh.elem_ref(tmp_vec[0]).n_nodes();

      connect.resize(tmp_vec.size()*num_nodes_per_elem);

      for (auto i : index_range(tmp_vec))
        {
          unsigned int elem_id = tmp_vec[i];
          libmesh_elem_num_to_exodus[elem_id] = ++libmesh_elem_num_to_exodus_counter; // 1-based indexing for Exodus

          const Elem & elem = mesh.elem_ref(elem_id);

          // We *might* be able to get away with writing mixed element
          // types which happen to have the same number of nodes, but
          // do we actually *want* to get away with that?
          // .) No visualization software would be able to handle it.
          // .) There'd be no way for us to read it back in reliably.
          // .) Even elements with the same number of nodes may have different connectivities (?)

          // This needs to be more than an assert so we don't fail
          // with a mysterious segfault while trying to write mixed
          // element meshes in optimized mode.
          libmesh_error_msg_if(elem.type() != conv.libmesh_elem_type(),
                               "Error: Exodus requires all elements with a given subdomain ID to be the same type.\n"
                               << "Can't write both "
                               << Utility::enum_to_string(elem.type())
                               << " and "
                               << Utility::enum_to_string(conv.libmesh_elem_type())
                               << " in the same block!");

          for (unsigned int j=0; j<static_cast<unsigned int>(num_nodes_per_elem); ++j)
            {
              unsigned int connect_index   = cast_int<unsigned int>((i*num_nodes_per_elem)+j);
              unsigned elem_node_index = conv.get_inverse_node_map(j); // inverse node map is for writing.
              if (!use_discontinuous)
                {
                  // The global id for the current node in libmesh.
                  dof_id_type libmesh_node_id = elem.node_id(elem_node_index);

                  // Write the Exodus global node id associated with
                  // this libmesh node number to the connectivity
                  // array, or throw an error if it's not found.
                  connect[connect_index] =
                    libmesh_map_find(libmesh_node_num_to_exodus,
                                     cast_int<int>(libmesh_node_id));
                }
              else
                {
                  // Look up the (elem_id, elem_node_index) pair in the map.
                  connect[connect_index] =
                    libmesh_map_find(discontinuous_node_indices,
                                     std::make_pair(elem_id, elem_node_index));
                }
            }
        }

      ex_error = exII::ex_put_conn
        (ex_id,
         exII::EX_ELEM_BLOCK,
         pr.first,
         connect.data(), // node_conn
         nullptr,        // elem_edge_conn (unused)
         nullptr);       // elem_face_conn (unused)
      EX_CHECK_ERR(ex_error, "Error writing element connectivities");

      // This transform command stores its result in a range that
      // begins at the third argument, so this command is adding
      // values to the elem_num_map vector starting from
      // curr_elem_map_end.  Here we add 1 to each id to make a
      // 1-based exodus file.
      curr_elem_map_end = std::transform
        (tmp_vec.begin(),
         tmp_vec.end(),
         curr_elem_map_end,
         [](dof_id_type id){return id+1;});
    }

  // write out the element number map that we created
  ex_error = exII::ex_put_elem_num_map(ex_id, elem_num_map.data());
  EX_CHECK_ERR(ex_error, "Error writing element map");

  // Write out the block names
  if (num_elem_blk > 0)
    {
      ex_error = exII::ex_put_names(ex_id, exII::EX_ELEM_BLOCK, names_table.get_char_star_star());
      EX_CHECK_ERR(ex_error, "Error writing element block names");
    }

  // Write out edge blocks if we have any
  for (const auto & pr : edge_id_to_conn)
    {
      ex_error = exII::ex_put_conn
        (ex_id,
         exII::EX_EDGE_BLOCK,
         pr.first,
         pr.second.data(), // node_conn
         nullptr,          // elem_edge_conn (unused)
         nullptr);         // elem_face_conn (unused)
      EX_CHECK_ERR(ex_error, "Error writing element connectivities");
    }

  // Write out the edge block names, if any.
  if (num_edge_blk > 0)
    {
      ex_error = exII::ex_put_names
        (ex_id,
         exII::EX_EDGE_BLOCK,
         edge_block_names_table.get_char_star_star());
      EX_CHECK_ERR(ex_error, "Error writing edge block names");
    }
}




void ExodusII_IO_Helper::write_sidesets(const MeshBase & mesh)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // Maps from sideset id to the element and sides
  std::map<int, std::vector<int>> elem;
  std::map<int, std::vector<int>> side;
  std::vector<boundary_id_type> side_boundary_ids;

  {
    // Accumulate the vectors to pass into ex_put_side_set
    // build_side_list() returns a vector of (elem, side, bc) tuples.
    for (const auto & t : mesh.get_boundary_info().build_side_list())
      {
        std::vector<const Elem *> family;
#ifdef LIBMESH_ENABLE_AMR
        /**
         * We need to build up active elements if AMR is enabled and add
         * them to the exodus sidesets instead of the potentially inactive "parent" elements
         */
        mesh.elem_ref(std::get<0>(t)).active_family_tree_by_side(family, std::get<1>(t), false);
#else
        family.push_back(mesh.elem_ptr(std::get<0>(t)));
#endif

        for (const auto & f : family)
          {
            const auto & conv = get_conversion(mesh.elem_ptr(f->id())->type());

            // Use the libmesh to exodus data structure map to get the proper sideset IDs
            // The data structure contains the "collapsed" contiguous ids
            elem[std::get<2>(t)].push_back(libmesh_elem_num_to_exodus[f->id()]);
            side[std::get<2>(t)].push_back(conv.get_inverse_side_map(std::get<1>(t)));
          }
      }

    mesh.get_boundary_info().build_side_boundary_ids(side_boundary_ids);
  }

  {
    // add data for shell faces, if needed

    // Accumulate the vectors to pass into ex_put_side_set
    for (const auto & t : mesh.get_boundary_info().build_shellface_list())
      {
        std::vector<const Elem *> family;
#ifdef LIBMESH_ENABLE_AMR
        /**
         * We need to build up active elements if AMR is enabled and add
         * them to the exodus sidesets instead of the potentially inactive "parent" elements
         */
        mesh.elem_ref(std::get<0>(t)).active_family_tree_by_side(family, std::get<1>(t), false);
#else
        family.push_back(mesh.elem_ptr(std::get<0>(t)));
#endif

        for (const auto & f : family)
          {
            const auto & conv = get_conversion(mesh.elem_ptr(f->id())->type());

            // Use the libmesh to exodus data structure map to get the proper sideset IDs
            // The data structure contains the "collapsed" contiguous ids
            elem[std::get<2>(t)].push_back(libmesh_elem_num_to_exodus[f->id()]);
            side[std::get<2>(t)].push_back(conv.get_inverse_shellface_map(std::get<1>(t)));
          }
      }

    std::vector<boundary_id_type> shellface_boundary_ids;
    mesh.get_boundary_info().build_shellface_boundary_ids(shellface_boundary_ids);
    for (const auto & id : shellface_boundary_ids)
      side_boundary_ids.push_back(id);
  }

  // Add any empty-but-named side boundary ids
  for (const auto & pair : mesh.get_boundary_info().get_sideset_name_map())
    {
      const boundary_id_type id = pair.first;

      if (std::find(side_boundary_ids.begin(),
                    side_boundary_ids.end(), id)
            == side_boundary_ids.end())
        side_boundary_ids.push_back(id);
    }

  // Write out the sideset names, but only if there is something to write
  if (side_boundary_ids.size() > 0)
    {
      NamesData names_table(side_boundary_ids.size(), MAX_STR_LENGTH);

      std::vector<exII::ex_set> sets(side_boundary_ids.size());

      for (auto i : index_range(side_boundary_ids))
        {
          boundary_id_type ss_id = side_boundary_ids[i];
          names_table.push_back_entry(mesh.get_boundary_info().get_sideset_name(ss_id));

          sets[i].id = ss_id;
          sets[i].type = exII::EX_SIDE_SET;
          sets[i].num_distribution_factor = 0;
          sets[i].distribution_factor_list = nullptr;

          auto elem_it = elem.find(ss_id);
          if (elem_it == elem.end())
            {
              sets[i].num_entry = 0;
              sets[i].entry_list = nullptr;
              sets[i].extra_list = nullptr;
            }
          else
            {
              sets[i].num_entry = elem_it->second.size();
              sets[i].entry_list = elem_it->second.data();
              sets[i].extra_list = libmesh_map_find(side, ss_id).data();
            }
        }

      ex_error = exII::ex_put_sets(ex_id, side_boundary_ids.size(), sets.data());
      EX_CHECK_ERR(ex_error, "Error writing sidesets");

      ex_error = exII::ex_put_names(ex_id, exII::EX_SIDE_SET, names_table.get_char_star_star());
      EX_CHECK_ERR(ex_error, "Error writing sideset names");
    }
}



void ExodusII_IO_Helper::write_nodesets(const MeshBase & mesh)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // build_node_list() builds a sorted list of (node-id, bc-id) tuples
  // that is sorted by node-id, but we actually want it to be sorted
  // by bc-id, i.e. the second argument of the tuple.
  auto bc_tuples =
    mesh.get_boundary_info().build_node_list();

  // We use std::stable_sort() here so that the entries within a
  // single nodeset remain sorted in node-id order, but now the
  // smallest boundary id's nodes appear first in the list, followed
  // by the second smallest, etc. That is, we are purposely doing two
  // different sorts here, with the first one being within the
  // build_node_list() call itself.
  std::stable_sort(bc_tuples.begin(), bc_tuples.end(),
                   [](const BoundaryInfo::NodeBCTuple & t1,
                      const BoundaryInfo::NodeBCTuple & t2)
                   { return std::get<1>(t1) < std::get<1>(t2); });

  std::vector<boundary_id_type> node_boundary_ids;
  mesh.get_boundary_info().build_node_boundary_ids(node_boundary_ids);

  // Add any empty-but-named node boundary ids
  for (const auto & pair : mesh.get_boundary_info().get_nodeset_name_map())
    {
      const boundary_id_type id = pair.first;

      if (std::find(node_boundary_ids.begin(),
                    node_boundary_ids.end(), id)
            == node_boundary_ids.end())
        node_boundary_ids.push_back(id);
    }

  // Write out the nodeset names, but only if there is something to write
  if (node_boundary_ids.size() > 0)
    {
      NamesData names_table(node_boundary_ids.size(), MAX_STR_LENGTH);

      // Vectors to be filled and passed to exII::ex_put_concat_sets()
      // Use existing class members and avoid variable shadowing.
      nodeset_ids.clear();
      num_nodes_per_set.clear();
      num_node_df_per_set.clear();
      node_sets_node_index.clear();
      node_sets_node_list.clear();

      // Pre-allocate space
      nodeset_ids.reserve(node_boundary_ids.size());
      num_nodes_per_set.reserve(node_boundary_ids.size());
      num_node_df_per_set.resize(node_boundary_ids.size()); // all zeros
      node_sets_node_index.reserve(node_boundary_ids.size());
      node_sets_node_list.reserve(bc_tuples.size());

      // Assign entries to node_sets_node_list, keeping track of counts as we go.
      std::map<boundary_id_type, unsigned int> nodeset_counts;
      for (auto id : node_boundary_ids)
        nodeset_counts[id] = 0;

      for (const auto & t : bc_tuples)
        {
          const dof_id_type & node_id = std::get<0>(t) + 1; // Note: we use 1-based node ids in Exodus!
          const boundary_id_type & nodeset_id = std::get<1>(t);
          node_sets_node_list.push_back(node_id);
          nodeset_counts[nodeset_id] += 1;
        }

      // Fill in other indexing vectors needed by Exodus
      unsigned int running_sum = 0;
      for (const auto & pr : nodeset_counts)
        {
          nodeset_ids.push_back(pr.first);
          num_nodes_per_set.push_back(pr.second);
          node_sets_node_index.push_back(running_sum);
          names_table.push_back_entry(mesh.get_boundary_info().get_nodeset_name(pr.first));
          running_sum += pr.second;
        }

      // Fill in an exII::ex_set_specs object which can then be passed to
      // the ex_put_concat_sets() function.
      exII::ex_set_specs set_data = {};
      set_data.sets_ids = nodeset_ids.data();
      set_data.num_entries_per_set = num_nodes_per_set.data();
      set_data.num_dist_per_set = num_node_df_per_set.data(); // zeros
      set_data.sets_entry_index = node_sets_node_index.data();
      set_data.sets_dist_index = node_sets_node_index.data(); // dummy value
      set_data.sets_entry_list = node_sets_node_list.data();

      // Write all nodesets together.
      ex_error = exII::ex_put_concat_sets(ex_id, exII::EX_NODE_SET, &set_data);
      EX_CHECK_ERR(ex_error, "Error writing concatenated nodesets");

      // Write out the nodeset names
      ex_error = exII::ex_put_names(ex_id, exII::EX_NODE_SET, names_table.get_char_star_star());
      EX_CHECK_ERR(ex_error, "Error writing nodeset names");
    }
}



void ExodusII_IO_Helper::initialize_element_variables(std::vector<std::string> names,
                                                      const std::vector<std::set<subdomain_id_type>> & vars_active_subdomains)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // Quick return if there are no element variables to write
  if (names.size() == 0)
    return;

  // Be sure that variables in the file match what we are asking for
  if (num_elem_vars > 0)
    {
      this->check_existing_vars(ELEMENTAL, names, this->elem_var_names);
      return;
    }

  // Quick return if we have already called this function
  if (_elem_vars_initialized)
    return;

  // Set the flag so we can skip this stuff on subsequent calls to
  // initialize_element_variables()
  _elem_vars_initialized = true;

  this->write_var_names(ELEMENTAL, names);

  // Use the truth table to indicate which subdomain/variable pairs are
  // active according to vars_active_subdomains.
  std::vector<int> truth_tab(num_elem_blk*num_elem_vars, 0);
  for (auto var_num : index_range(vars_active_subdomains))
    {
      // If the list of active subdomains is empty, it is interpreted as being
      // active on *all* subdomains.
      std::set<subdomain_id_type> current_set;
      if (vars_active_subdomains[var_num].empty())
        for (auto block_id : block_ids)
          current_set.insert(cast_int<subdomain_id_type>(block_id));
      else
        current_set = vars_active_subdomains[var_num];

      // Find index into the truth table for each id in current_set.
      for (auto block_id : current_set)
        {
          auto it = std::find(block_ids.begin(), block_ids.end(), block_id);
          libmesh_error_msg_if(it == block_ids.end(),
                               "ExodusII_IO_Helper: block id " << block_id << " not found in block_ids.");

          std::size_t block_index =
            std::distance(block_ids.begin(), it);

          std::size_t truth_tab_index = block_index*num_elem_vars + var_num;
          truth_tab[truth_tab_index] = 1;
        }
    }

  ex_error = exII::ex_put_elem_var_tab(ex_id,
                                     num_elem_blk,
                                     num_elem_vars,
                                     truth_tab.data());
  EX_CHECK_ERR(ex_error, "Error writing element truth table.");
}



void ExodusII_IO_Helper::initialize_nodal_variables(std::vector<std::string> names)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // Quick return if there are no nodal variables to write
  if (names.size() == 0)
    return;

  // Quick return if we have already called this function
  if (_nodal_vars_initialized)
    return;

  // Be sure that variables in the file match what we are asking for
  if (num_nodal_vars > 0)
    {
      this->check_existing_vars(NODAL, names, this->nodal_var_names);
      return;
    }

  // Set the flag so we can skip the rest of this function on subsequent calls.
  _nodal_vars_initialized = true;

  this->write_var_names(NODAL, names);
}



void ExodusII_IO_Helper::initialize_global_variables(std::vector<std::string> names)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // Quick return if there are no global variables to write
  if (names.size() == 0)
    return;

  if (_global_vars_initialized)
    return;

  // Be sure that variables in the file match what we are asking for
  if (num_global_vars > 0)
    {
      this->check_existing_vars(GLOBAL, names, this->global_var_names);
      return;
    }

  _global_vars_initialized = true;

  this->write_var_names(GLOBAL, names);
}



void ExodusII_IO_Helper::check_existing_vars(ExodusVarType type,
                                             std::vector<std::string> & names,
                                             std::vector<std::string> & names_from_file)
{
  // There may already be global variables in the file (for example,
  // if we're appending) and in that case, we
  // 1.) Cannot initialize them again.
  // 2.) Should check to be sure that the global variable names are the same.

  // Fills up names_from_file for us
  this->read_var_names(type);

  // Both the number of variables and their names (up to the first
  // MAX_STR_LENGTH characters) must match for the names we are
  // planning to write and the names already in the file.
  bool match =
    std::equal(names.begin(), names.end(),
               names_from_file.begin(),
               [](const std::string & a,
                  const std::string & b) -> bool
               {
                 return a.compare(/*pos=*/0, /*len=*/MAX_STR_LENGTH, b) == 0;
               });

  if (!match)
    {
      libMesh::err << "Error! The Exodus file already contains the variables:" << std::endl;
      for (const auto & name : names_from_file)
        libMesh::err << name << std::endl;

      libMesh::err << "And you asked to write:" << std::endl;
      for (const auto & name : names)
        libMesh::err << name << std::endl;

      libmesh_error_msg("Cannot overwrite existing variables in Exodus II file.");
    }
}



void ExodusII_IO_Helper::write_timestep(int timestep, Real time)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  if (_single_precision)
    {
      float cast_time = float(time);
      ex_error = exII::ex_put_time(ex_id, timestep, &cast_time);
    }
  else
    {
      double cast_time = double(time);
      ex_error = exII::ex_put_time(ex_id, timestep, &cast_time);
    }
  EX_CHECK_ERR(ex_error, "Error writing timestep.");

  this->update();
}



void
ExodusII_IO_Helper::
write_sideset_data(const MeshBase & mesh,
                   int timestep,
                   const std::vector<std::string> & var_names,
                   const std::vector<std::set<boundary_id_type>> & side_ids,
                   const std::vector<std::map<BoundaryInfo::BCTuple, Real>> & bc_vals)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // Write the sideset variable names to file. This function should
  // only be called once for SIDESET variables, repeated calls to
  // write_var_names overwrites/changes the order of names that were
  // there previously, and will mess up any data that has already been
  // written.
  this->write_var_names(SIDESET, var_names);

  // I hope that we are allowed to call read_sideset_info() even
  // though we are in the middle of writing? It seems to work provided
  // that you have already written the mesh itself... read_sideset_info()
  // fills in the following data members:
  // .) num_side_sets
  // .) ss_ids
  this->read_sideset_info();

  // Write "truth" table for sideset variables.  The function
  // exII::ex_put_var_param() must be called before
  // exII::ex_put_sset_var_tab(). For us, this happens during the call
  // to ExodusII_IO_Helper::write_var_names(). sset_var_tab is a logically
  // (num_side_sets x num_sset_var) integer array of 0s and 1s
  // indicating which sidesets a given sideset variable is defined on.
  std::vector<int> sset_var_tab(num_side_sets * var_names.size());

  // We now call read_sideset() once per sideset and write any sideset
  // variable values which are defined there.
  int offset=0;
  for (int ss=0; ss<num_side_sets; ++ss)
    {
      // We don't know num_sides_per_set for each set until we call
      // read_sideset(). The values for each sideset are stored (using
      // the offsets) into the 'elem_list' and 'side_list' arrays of
      // this class.
      offset += (ss > 0 ? num_sides_per_set[ss-1] : 0);
      this->read_sideset(ss, offset);

      // For each variable in var_names, write the values for the
      // current sideset, if any.
      for (auto var : index_range(var_names))
        {
          // If this var has no values on this sideset, go to the next one.
          if (!side_ids[var].count(ss_ids[ss]))
            continue;

          // Otherwise, fill in this entry of the sideset truth table.
          sset_var_tab[ss*var_names.size() + var] = 1;

          // Data vector that will eventually be passed to exII::ex_put_sset_var().
          std::vector<Real> sset_var_vals(num_sides_per_set[ss]);

          // Get reference to the BCTuple -> Real map for this variable.
          const auto & data_map = bc_vals[var];

          // Loop over elem_list, side_list entries in current sideset.
          for (int i=0; i<num_sides_per_set[ss]; ++i)
            {
              // Get elem_id and side_id from the respective lists that
              // are filled in by calling read_sideset().
              //
              // Note: these are Exodus-specific ids, so we have to convert them
              // to libmesh ids, as that is what will be in the bc_tuples.
              //
              // TODO: we should probably consult the exodus_elem_num_to_libmesh
              // mapping in order to figure out which libmesh element id 'elem_id'
              // actually corresponds to here, instead of just assuming it will be
              // off by one. Unfortunately that data structure does not seem to
              // be used at the moment. If we assume that write_sideset_data() is
              // always called following write(), then this should be a fairly safe
              // assumption...
              dof_id_type elem_id = elem_list[i + offset] - 1;
              unsigned int side_id = side_list[i + offset] - 1;

              // Sanity check: make sure that the "off by one"
              // assumption we used above to set 'elem_id' is valid.
              libmesh_error_msg_if
                (libmesh_map_find(libmesh_elem_num_to_exodus, cast_int<int>(elem_id)) !=
                 cast_int<dof_id_type>(elem_list[i + offset]),
                 "Error mapping Exodus elem id to libmesh elem id.");

              // Map from Exodus side ids to libmesh side ids.
              const auto & conv = get_conversion(mesh.elem_ptr(elem_id)->type());

              // Map from Exodus side ids to libmesh side ids.
              unsigned int converted_side_id = conv.get_side_map(side_id);

              // Construct a key so we can quickly see whether there is any
              // data for this variable in the map.
              BoundaryInfo::BCTuple key = std::make_tuple
                (elem_id,
                 converted_side_id,
                 ss_ids[ss]);

              // Find the data for this (elem,side,id) tuple. Throw an
              // error if not found. Then store value in vector which
              // will be passed to Exodus.
              sset_var_vals[i] = libmesh_map_find(data_map, key);
            } // end for (i)

          // As far as I can tell, there is no "concat" version of writing
          // sideset data, you have to call ex_put_sset_var() once per (variable,
          // sideset) pair.
          if (sset_var_vals.size() > 0)
            {
              ex_error = exII::ex_put_sset_var
                (ex_id,
                 timestep,
                 var + 1, // 1-based variable index of current variable
                 ss_ids[ss],
                 num_sides_per_set[ss],
                 MappedOutputVector(sset_var_vals, _single_precision).data());
              EX_CHECK_ERR(ex_error, "Error writing sideset vars.");
            }
        } // end for (var)
    } // end for (ss)

  // Finally, write the sideset truth table.
  ex_error =
    exII::ex_put_sset_var_tab(ex_id,
                              num_side_sets,
                              cast_int<int>(var_names.size()),
                              sset_var_tab.data());
  EX_CHECK_ERR(ex_error, "Error writing sideset var truth table.");
}



void
ExodusII_IO_Helper::
read_sideset_data(const MeshBase & mesh,
                  int timestep,
                  std::vector<std::string> & var_names,
                  std::vector<std::set<boundary_id_type>> & side_ids,
                  std::vector<std::map<BoundaryInfo::BCTuple, Real>> & bc_vals)
{
  // This reads the sideset variable names into the local
  // sideset_var_names data structure.
  this->read_var_names(SIDESET);

  if (num_sideset_vars)
    {
      // Read the sideset data truth table
      std::vector<int> sset_var_tab(num_side_sets * num_sideset_vars);
      ex_error = exII::ex_get_sset_var_tab
        (ex_id,
         num_side_sets,
         num_sideset_vars,
         sset_var_tab.data());
      EX_CHECK_ERR(ex_error, "Error reading sideset variable truth table.");

      // Set up/allocate space in incoming data structures.
      var_names = sideset_var_names;
      side_ids.resize(num_sideset_vars);
      bc_vals.resize(num_sideset_vars);

      // Read the sideset data.
      //
      // Note: we assume that read_sideset() has already been called
      // for each sideset, so the required values in elem_list and
      // side_list are already present.
      //
      // TODO: As a future optimization, we could read only the values
      // requested by the user by looking at the input parameter
      // var_names and checking whether it already has entries in
      // it. We could do the same thing with the input side_ids
      // container and only read values for requested sidesets.
      int offset=0;
      for (int ss=0; ss<num_side_sets; ++ss)
        {
          offset += (ss > 0 ? num_sides_per_set[ss-1] : 0);
          for (int var=0; var<num_sideset_vars; ++var)
            {
              int is_present = sset_var_tab[num_sideset_vars*ss + var];

              if (is_present)
                {
                  // Record the fact that this variable is defined on this sideset.
                  side_ids[var].insert(ss_ids[ss]);

                  // Note: the assumption here is that a previous call
                  // to this->read_sideset_info() has already set the
                  // values of num_sides_per_set, so we just use those values here.
                  std::vector<Real> sset_var_vals(num_sides_per_set[ss]);
                  ex_error = exII::ex_get_sset_var
                    (ex_id,
                     timestep,
                     var + 1, // 1-based sideset variable index!
                     ss_ids[ss],
                     num_sides_per_set[ss],
                     MappedInputVector(sset_var_vals, _single_precision).data());
                  EX_CHECK_ERR(ex_error, "Error reading sideset variable.");

                  for (int i=0; i<num_sides_per_set[ss]; ++i)
                    {
                      dof_id_type exodus_elem_id = elem_list[i + offset];
                      unsigned int exodus_side_id = side_list[i + offset];

                      // FIXME: We should use exodus_elem_num_to_libmesh for this,
                      // but it apparently is never set up, so just
                      // subtract 1 from the Exodus elem id.
                      dof_id_type converted_elem_id = exodus_elem_id - 1;

                      // Map Exodus side id to libmesh side id.
                      // Map from Exodus side ids to libmesh side ids.
                      const auto & conv = get_conversion(mesh.elem_ptr(converted_elem_id)->type());

                      // Map from Exodus side id to libmesh side id.
                      // Note: the mapping is defined on 0-based indices, so subtract
                      // 1 before doing the mapping.
                      unsigned int converted_side_id = conv.get_side_map(exodus_side_id - 1);

                      // Make a BCTuple key from the converted information.
                      BoundaryInfo::BCTuple key = std::make_tuple
                        (converted_elem_id,
                         converted_side_id,
                         ss_ids[ss]);

                      // Store (elem, side, b_id) tuples in bc_vals[var]
                      bc_vals[var].emplace(key, sset_var_vals[i]);
                    } // end for (i)
                } // end if (present)
            } // end for (var)
        } // end for (ss)
    } // end if (num_sideset_vars)
}



void ExodusII_IO_Helper::
write_nodeset_data (int timestep,
                    const std::vector<std::string> & var_names,
                    const std::vector<std::set<boundary_id_type>> & node_boundary_ids,
                    std::vector<std::map<BoundaryInfo::NodeBCTuple, Real>> & bc_vals)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // Write the nodeset variable names to file. This function should
  // only be called once for NODESET variables, repeated calls to
  // write_var_names() overwrites/changes the order of names that were
  // there previously, and will mess up any data that has already been
  // written.
  this->write_var_names(NODESET, var_names);

  // For all nodesets, reads and fills in the arrays:
  // nodeset_ids
  // num_nodes_per_set
  // node_sets_node_index - starting index for each nodeset in the node_sets_node_list vector
  // node_sets_node_list
  // Note: we need these arrays so that we know what data to write
  this->read_all_nodesets();

  // The "truth" table for nodeset variables. nset_var_tab is a
  // logically (num_node_sets x num_nset_var) integer array of 0s and
  // 1s indicating which nodesets a given nodeset variable is defined
  // on.
  std::vector<int> nset_var_tab(num_node_sets * var_names.size());

  for (int ns=0; ns<num_node_sets; ++ns)
  {
    // The offset into the node_sets_node_list for the current nodeset
    int offset = node_sets_node_index[ns];

    // For each variable in var_names, write the values for the
    // current nodeset, if any.
    for (auto var : index_range(var_names))
      {
        // If this var has no values on this nodeset, go to the next one.
        if (!node_boundary_ids[var].count(nodeset_ids[ns]))
          continue;

        // Otherwise, fill in this entry of the nodeset truth table.
        nset_var_tab[ns*var_names.size() + var] = 1;

        // Data vector that will eventually be passed to exII::ex_put_nset_var().
        std::vector<Real> nset_var_vals(num_nodes_per_set[ns]);

        // Get reference to the NodeBCTuple -> Real map for this variable.
        const auto & data_map = bc_vals[var];

        // Loop over entries in current nodeset.
        for (int i=0; i<num_nodes_per_set[ns]; ++i)
          {
            // Here we convert Exodus node ids to libMesh node ids by
            // subtracting 1.  We should probably use the
            // exodus_node_num_to_libmesh data structure for this, but
            // I don't think it is set up at the time when
            // write_nodeset_data() would normally be called.
            dof_id_type libmesh_node_id = node_sets_node_list[i + offset] - 1;

            // Construct a key to look up values in data_map.
            BoundaryInfo::NodeBCTuple key =
              std::make_tuple(libmesh_node_id, nodeset_ids[ns]);

            // We require that the user provided either no values for
            // this (var, nodeset) combination (in which case we don't
            // reach this point) or a value for _every_ node in this
            // nodeset for this var, so we use the libmesh_map_find()
            // macro to check for this.
            nset_var_vals[i] = libmesh_map_find(data_map, key);
          } // end for (node in nodeset[ns])

        // Write nodeset values to Exodus file
        if (nset_var_vals.size() > 0)
          {
            ex_error = exII::ex_put_nset_var
              (ex_id,
               timestep,
               var + 1, // 1-based variable index of current variable
               nodeset_ids[ns],
               num_nodes_per_set[ns],
               MappedOutputVector(nset_var_vals, _single_precision).data());
            EX_CHECK_ERR(ex_error, "Error writing nodeset vars.");
          }
      } // end for (var in var_names)
  } // end for (ns)

  // Finally, write the nodeset truth table.
  ex_error =
    exII::ex_put_nset_var_tab(ex_id,
                              num_node_sets,
                              cast_int<int>(var_names.size()),
                              nset_var_tab.data());
  EX_CHECK_ERR(ex_error, "Error writing nodeset var truth table.");
}



void ExodusII_IO_Helper::
read_nodeset_data (int timestep,
                   std::vector<std::string> & var_names,
                   std::vector<std::set<boundary_id_type>> & node_boundary_ids,
                   std::vector<std::map<BoundaryInfo::NodeBCTuple, Real>> & bc_vals)
{
  // This reads the sideset variable names into the local
  // sideset_var_names data structure.
  this->read_var_names(NODESET);

  if (num_nodeset_vars)
    {
      // Read the nodeset data truth table
      std::vector<int> nset_var_tab(num_node_sets * num_nodeset_vars);
      ex_error = exII::ex_get_nset_var_tab
        (ex_id,
         num_node_sets,
         num_nodeset_vars,
         nset_var_tab.data());
      EX_CHECK_ERR(ex_error, "Error reading nodeset variable truth table.");

      // Set up/allocate space in incoming data structures.
      var_names = nodeset_var_names;
      node_boundary_ids.resize(num_nodeset_vars);
      bc_vals.resize(num_nodeset_vars);

      // Read the nodeset data.
      //
      // Note: we assume that the functions
      // 1.) this->read_nodeset_info() and
      // 2.) this->read_all_nodesets()
      // have already been called, so that we already know e.g. how
      // many nodes are in each set, their ids, etc.
      //
      // TODO: As a future optimization, we could read only the values
      // requested by the user by looking at the input parameter
      // var_names and checking whether it already has entries in
      // it.
      int offset=0;
      for (int ns=0; ns<num_node_sets; ++ns)
        {
          offset += (ns > 0 ? num_nodes_per_set[ns-1] : 0);
          for (int var=0; var<num_nodeset_vars; ++var)
            {
              int is_present = nset_var_tab[num_nodeset_vars*ns + var];

              if (is_present)
                {
                  // Record the fact that this variable is defined on this nodeset.
                  node_boundary_ids[var].insert(nodeset_ids[ns]);

                  // Note: the assumption here is that a previous call
                  // to this->read_nodeset_info() has already set the
                  // values of num_nodes_per_set, so we just use those values here.
                  std::vector<Real> nset_var_vals(num_nodes_per_set[ns]);
                  ex_error = exII::ex_get_nset_var
                    (ex_id,
                     timestep,
                     var + 1, // 1-based nodeset variable index!
                     nodeset_ids[ns],
                     num_nodes_per_set[ns],
                     MappedInputVector(nset_var_vals, _single_precision).data());
                  EX_CHECK_ERR(ex_error, "Error reading nodeset variable.");

                  for (int i=0; i<num_nodes_per_set[ns]; ++i)
                    {
                      // The read_all_nodesets() function now reads all the node ids into the
                      // node_sets_node_list vector, which is of length "total_nodes_in_all_sets"
                      // The old read_nodset() function is no longer called as far as I can tell,
                      // and should probably be removed? The "offset" that we are using only
                      // depends on the current nodeset index and the num_nodes_per_set vector,
                      // which gets filled in by the call to read_all_nodesets().
                      dof_id_type exodus_node_id = node_sets_node_list[i + offset];

                      // FIXME: We should use exodus_node_num_to_libmesh for this,
                      // but it apparently is never set up, so just
                      // subtract 1 from the Exodus node id.
                      dof_id_type converted_node_id = exodus_node_id - 1;

                      // Make a NodeBCTuple key from the converted information.
                      BoundaryInfo::NodeBCTuple key = std::make_tuple
                        (converted_node_id, nodeset_ids[ns]);

                      // Store (node, b_id) tuples in bc_vals[var]
                      bc_vals[var].emplace(key, nset_var_vals[i]);
                    } // end for (i)
                } // end if (present)
            } // end for (var)
        } // end for (ns)
    } // end if (num_nodeset_vars)
}



void ExodusII_IO_Helper::write_element_values
(const MeshBase & mesh,
 const std::vector<Real> & values,
 int timestep,
 const std::vector<std::set<subdomain_id_type>> & vars_active_subdomains)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // Ask the file how many element vars it has, store it in the num_elem_vars variable.
  ex_error = exII::ex_get_var_param(ex_id, "e", &num_elem_vars);
  EX_CHECK_ERR(ex_error, "Error reading number of elemental variables.");

  // We will eventually loop over the element blocks (subdomains) and
  // write the data one block at a time. Build a data structure that
  // maps each subdomain to a list of element ids it contains.
  std::map<subdomain_id_type, std::vector<unsigned int>> subdomain_map;
  for (const auto & elem : mesh.active_element_ptr_range())
    subdomain_map[elem->subdomain_id()].push_back(elem->id());

  // Use mesh.n_elem() to access into the values vector rather than
  // the number of elements the Exodus writer thinks the mesh has,
  // which may not include inactive elements.
  dof_id_type n_elem = mesh.n_elem();

  // Sanity check: we must have an entry in vars_active_subdomains for
  // each variable that we are potentially writing out.
  libmesh_assert_equal_to
    (vars_active_subdomains.size(),
     static_cast<unsigned>(num_elem_vars));

  // For each variable, create a 'data' array which holds all the elemental variable
  // values *for a given block* on this processor, then write that data vector to file
  // before moving onto the next block.
  for (unsigned int var_id=0; var_id<static_cast<unsigned>(num_elem_vars); ++var_id)
    {
      // The size of the subdomain map is the number of blocks.
      auto it = subdomain_map.begin();

      // Reference to the set of active subdomains for the current variable.
      const auto & active_subdomains
        = vars_active_subdomains[var_id];

      for (unsigned int j=0; it!=subdomain_map.end(); ++it, ++j)
        {
          // Skip any variable/subdomain pairs that are inactive.
          // Note that if active_subdomains is empty, it is interpreted
          // as being active on *all* subdomains.
          if (!(active_subdomains.empty() || active_subdomains.count(it->first)))
            continue;

          // Get reference to list of elem ids which are in the
          // current subdomain and count, allocate storage to hold
          // data that will be written to file.
          const auto & elem_nums = it->second;
          const unsigned int num_elems_this_block =
            cast_int<unsigned int>(elem_nums.size());
          std::vector<Real> data(num_elems_this_block);

          // variable-major ordering is:
          // (u1, u2, u3, ..., uN), (v1, v2, v3, ..., vN), ...
          // where N is the number of elements.
          for (unsigned int k=0; k<num_elems_this_block; ++k)
            data[k] = values[var_id*n_elem + elem_nums[k]];

          ex_error = exII::ex_put_elem_var
            (ex_id,
             timestep,
             var_id+1,
             this->get_block_id(j),
             num_elems_this_block,
             MappedOutputVector(data, _single_precision).data());

          EX_CHECK_ERR(ex_error, "Error writing element values.");
        }
    }

  this->update();
}



void ExodusII_IO_Helper::write_element_values_element_major
(const MeshBase & mesh,
 const std::vector<Real> & values,
 int timestep,
 const std::vector<std::set<subdomain_id_type>> & vars_active_subdomains,
 const std::vector<std::string> & derived_var_names,
 const std::map<subdomain_id_type, std::vector<std::string>> & subdomain_to_var_names)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // Ask the file how many element vars it has, store it in the num_elem_vars variable.
  ex_error = exII::ex_get_var_param(ex_id, "e", &num_elem_vars);
  EX_CHECK_ERR(ex_error, "Error reading number of elemental variables.");

  // We will eventually loop over the element blocks (subdomains) and
  // write the data one block (subdomain) at a time. Build a data
  // structure that keeps track of how many elements are in each
  // subdomain. This will allow us to reserve space in the data vector
  // we are going to write.
  std::map<subdomain_id_type, unsigned int> subdomain_to_n_elem;
  for (const auto & elem : mesh.active_element_ptr_range())
    subdomain_to_n_elem[elem->subdomain_id()] += 1;

  // Sanity check: we must have an entry in vars_active_subdomains for
  // each variable that we are potentially writing out.
  libmesh_assert_equal_to
    (vars_active_subdomains.size(),
     static_cast<unsigned>(num_elem_vars));

  // The size of the subdomain map is the number of blocks.
  auto subdomain_to_n_elem_iter = subdomain_to_n_elem.begin();

  // Store range of active Elem pointers. We are going to loop over
  // the elements n_vars * n_subdomains times, so let's make sure
  // the predicated iterators aren't slowing us down too much.
  ConstElemRange elem_range
    (mesh.active_elements_begin(),
     mesh.active_elements_end());

  for (unsigned int sbd_idx=0;
       subdomain_to_n_elem_iter != subdomain_to_n_elem.end();
       ++subdomain_to_n_elem_iter, ++sbd_idx)
    for (unsigned int var_id=0; var_id<static_cast<unsigned>(num_elem_vars); ++var_id)
      {
        // Reference to the set of active subdomains for the current variable.
        const auto & active_subdomains
          = vars_active_subdomains[var_id];

        // If the vars_active_subdomains container passed to this function
        // has an empty entry, it means the variable really is not active on
        // _any_ subdomains, not that it is active on _all_ subdomains. This
        // is just due to the way that we build the vars_active_subdomains
        // container.
        if (!active_subdomains.count(subdomain_to_n_elem_iter->first))
          continue;

        // Vector to hold values that will be written to Exodus file.
        std::vector<Real> data;
        data.reserve(subdomain_to_n_elem_iter->second);

        unsigned int values_offset = 0;
        for (auto & elem : elem_range)
          {
            // We'll use the Elem's subdomain id in several places below.
            subdomain_id_type sbd_id = elem->subdomain_id();

            // Get reference to the list of variable names defining
            // the indexing for the current Elem's subdomain.
            auto subdomain_to_var_names_iter =
              subdomain_to_var_names.find(sbd_id);

            // It's possible, but unusual, for there to be an Elem
            // from a subdomain that has no active variables from the
            // set of variables we are currently writing. If that
            // happens, we can just go to the next Elem because we
            // don't need to advance the offset into the values
            // vector, etc.
            if (subdomain_to_var_names_iter == subdomain_to_var_names.end())
              continue;

            const auto & var_names_this_sbd
              = subdomain_to_var_names_iter->second;

            // Only extract values if Elem is in the current subdomain.
            if (sbd_id == subdomain_to_n_elem_iter->first)
              {
                // Location of current var_id in the list of all variables on this
                // subdomain. FIXME: linear search but it's over a typically relatively
                // short vector of active variable names on this subdomain. We could do
                // a nested std::map<string,index> instead of a std::vector where the
                // location of the string is implicitly the index..
                auto pos =
                  std::find(var_names_this_sbd.begin(),
                            var_names_this_sbd.end(),
                            derived_var_names[var_id]);

                libmesh_error_msg_if(pos == var_names_this_sbd.end(),
                                     "Derived name " << derived_var_names[var_id] << " not found!");

                // Find the current variable's location in the list of all variable
                // names on the current Elem's subdomain.
                auto true_index =
                  std::distance(var_names_this_sbd.begin(), pos);

                data.push_back(values[values_offset + true_index]);
              }

            // The "true" offset is how much we have to advance the index for each Elem
            // in this subdomain.
            auto true_offset = var_names_this_sbd.size();

            // Increment to the next Elem's values
            values_offset += true_offset;
          } // for elem

        // Now write 'data' to Exodus file, in single precision if requested.
        if (!data.empty())
          {
            ex_error = exII::ex_put_elem_var
              (ex_id, timestep, var_id+1, this->get_block_id(sbd_idx), data.size(),
               MappedOutputVector(data, _single_precision).data());

            EX_CHECK_ERR(ex_error, "Error writing element values.");
          }
      } // for each var_id

  this->update();
}



void
ExodusII_IO_Helper::write_nodal_values(int var_id,
                                       const std::vector<Real> & values,
                                       int timestep)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  if (!values.empty())
    {
      ex_error = exII::ex_put_nodal_var
        (ex_id, timestep, var_id, num_nodes,
         MappedOutputVector(values, _single_precision).data());

      EX_CHECK_ERR(ex_error, "Error writing nodal values.");

      this->update();
    }
}



void ExodusII_IO_Helper::write_information_records(const std::vector<std::string> & records)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  // There may already be information records in the file (for
  // example, if we're appending) and in that case, according to the
  // Exodus documentation, writing more information records is not
  // supported.
  int num_info = inquire(*this, exII::EX_INQ_INFO, "Error retrieving the number of information records from file!");
  if (num_info > 0)
    {
      libMesh::err << "Warning! The Exodus file already contains information records.\n"
                   << "Exodus does not support writing additional records in this situation."
                   << std::endl;
      return;
    }

  int num_records = cast_int<int>(records.size());

  if (num_records > 0)
    {
      NamesData info(num_records, MAX_LINE_LENGTH);

      // If an entry is longer than MAX_LINE_LENGTH characters it's not an error, we just
      // write the first MAX_LINE_LENGTH characters to the file.
      for (const auto & record : records)
        info.push_back_entry(record);

      ex_error = exII::ex_put_info(ex_id, num_records, info.get_char_star_star());
      EX_CHECK_ERR(ex_error, "Error writing global values.");

      this->update();
    }
}



void ExodusII_IO_Helper::write_global_values(const std::vector<Real> & values, int timestep)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  if (!values.empty())
    {
      ex_error = exII::ex_put_glob_vars
        (ex_id, timestep, num_global_vars,
         MappedOutputVector(values, _single_precision).data());

      EX_CHECK_ERR(ex_error, "Error writing global values.");

      this->update();
    }
}



void ExodusII_IO_Helper::update()
{
  ex_error = exII::ex_update(ex_id);
  EX_CHECK_ERR(ex_error, "Error flushing buffers to file.");
}



void ExodusII_IO_Helper::read_global_values(std::vector<Real> & values, int timestep)
{
  if ((_run_only_on_proc0) && (this->processor_id() != 0))
    return;

  values.clear();
  values.resize(num_global_vars);
  ex_error = exII::ex_get_glob_vars
    (ex_id, timestep, num_global_vars,
     MappedInputVector(values, _single_precision).data());

  EX_CHECK_ERR(ex_error, "Error reading global values.");
}



void ExodusII_IO_Helper::use_mesh_dimension_instead_of_spatial_dimension(bool val)
{
  _use_mesh_dimension_instead_of_spatial_dimension = val;
}



void ExodusII_IO_Helper::write_as_dimension(unsigned dim)
{
  _write_as_dimension = dim;
}



void ExodusII_IO_Helper::set_coordinate_offset(Point p)
{
  _coordinate_offset = p;
}


std::vector<std::string>
ExodusII_IO_Helper::get_complex_names(const std::vector<std::string> & names,
                                      bool write_complex_abs) const
{
  std::vector<std::string> complex_names;

  // This will loop over all names and create new "complex" names
  // (i.e. names that start with r_, i_ or a_)
  for (const auto & name : names)
    {
      complex_names.push_back("r_" + name);
      complex_names.push_back("i_" + name);
      if (write_complex_abs)
        complex_names.push_back("a_" + name);
    }

  return complex_names;
}



std::vector<std::set<subdomain_id_type>>
ExodusII_IO_Helper::
get_complex_vars_active_subdomains
(const std::vector<std::set<subdomain_id_type>> & vars_active_subdomains,
 bool write_complex_abs) const
{
  std::vector<std::set<subdomain_id_type>> complex_vars_active_subdomains;

  for (auto & s : vars_active_subdomains)
    {
      // Push back the same data enough times for the real, imag, (and
      // possibly modulus) for the complex-valued solution.
      complex_vars_active_subdomains.push_back(s);
      complex_vars_active_subdomains.push_back(s);
      if (write_complex_abs)
        complex_vars_active_subdomains.push_back(s);
    }

  return complex_vars_active_subdomains;
}



std::map<subdomain_id_type, std::vector<std::string>>
ExodusII_IO_Helper::
get_complex_subdomain_to_var_names
(const std::map<subdomain_id_type, std::vector<std::string>> & subdomain_to_var_names,
 bool write_complex_abs) const
{
  // Eventual return value
  std::map<subdomain_id_type, std::vector<std::string>> ret;

  unsigned int num_complex_outputs = write_complex_abs ? 3 : 2;

  for (const auto & pr : subdomain_to_var_names)
    {
      // Initialize entry for current subdomain
      auto & vec = ret[pr.first];

      // Get list of non-complex variable names active on this subdomain.
      const auto & varnames = pr.second;

      // Allocate space for the complex-valued entries
      vec.reserve(num_complex_outputs * varnames.size());

      // For each varname in the input map, write three variable names
      // to the output formed by prepending "r_", "i_", and "a_",
      // respectively.
      for (const auto & varname : varnames)
        {
          vec.push_back("r_" + varname);
          vec.push_back("i_" + varname);
          if (write_complex_abs)
            vec.push_back("a_" + varname);
        }
    }
  return ret;
}



int ExodusII_IO_Helper::Conversion::get_node_map(int i) const
{
  if (!node_map)
    return i;

  libmesh_assert_less (i, node_map->size());
  return (*node_map)[i];
}



int ExodusII_IO_Helper::Conversion::get_inverse_node_map(int i) const
{
  if (!inverse_node_map)
    return i;

  libmesh_assert_less (i, inverse_node_map->size());
  return (*inverse_node_map)[i];
}



int ExodusII_IO_Helper::Conversion::get_side_map(int i) const
{
  if (!side_map)
    return i;

  // If we asked for a side that doesn't exist, return an invalid_id
  // and allow higher-level code to handle it.
  if (static_cast<size_t>(i) >= side_map->size())
    return invalid_id;

  return (*side_map)[i];
}



int ExodusII_IO_Helper::Conversion::get_inverse_side_map(int i) const
{
  // For identity side mappings, we our convention is to return a 1-based index.
  if (!inverse_side_map)
    return i + 1;

  libmesh_assert_less (i, inverse_side_map->size());
  return (*inverse_side_map)[i];
}



/**
 * \returns The ith component of the shellface map for this element.
 * \note Nothing is currently using this.
 */
int ExodusII_IO_Helper::Conversion::get_shellface_map(int i) const
{
  if (!shellface_map)
    return i;

  libmesh_assert_less (i, shellface_map->size());
  return (*shellface_map)[i];
}



int ExodusII_IO_Helper::Conversion::get_inverse_shellface_map(int i) const
{
  if (!inverse_shellface_map)
    return i + 1;

  libmesh_assert_less (i, inverse_shellface_map->size());
  return (*inverse_shellface_map)[i];
}



ElemType ExodusII_IO_Helper::Conversion::libmesh_elem_type() const
{
  return libmesh_type;
}



std::string ExodusII_IO_Helper::Conversion::exodus_elem_type() const
{
  return exodus_type;
}



/**
 * \returns The shellface index offset.
 */
std::size_t ExodusII_IO_Helper::Conversion::get_shellface_index_offset() const
{
  return shellface_index_offset;
}

ExodusII_IO_Helper::NamesData::NamesData(size_t n_strings, size_t string_length) :
  data_table(n_strings),
  data_table_pointers(n_strings),
  counter(0),
  table_size(n_strings)
{
  for (size_t i=0; i<n_strings; ++i)
    {
      data_table[i].resize(string_length + 1);

      // Properly terminate these C-style strings, just to be safe.
      data_table[i][0] = '\0';

      // Set pointer into the data_table
      data_table_pointers[i] = data_table[i].data();
    }
}



void ExodusII_IO_Helper::NamesData::push_back_entry(const std::string & name)
{
  libmesh_assert_less (counter, table_size);

  // 1.) Copy the C++ string into the vector<char>...
  size_t num_copied = name.copy(data_table[counter].data(), data_table[counter].size()-1);

  // 2.) ...And null-terminate it.
  data_table[counter][num_copied] = '\0';

  // Go to next row
  ++counter;
}



char ** ExodusII_IO_Helper::NamesData::get_char_star_star()
{
  return data_table_pointers.data();
}



char * ExodusII_IO_Helper::NamesData::get_char_star(int i)
{
  libmesh_error_msg_if(static_cast<unsigned>(i) >= table_size,
                       "Requested char * " << i << " but only have " << table_size << "!");

  return data_table[i].data();
}


} // namespace libMesh



#endif // #ifdef LIBMESH_HAVE_EXODUS_API
